diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c44fad0..87c1de0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## Fleet 3.3.0 (Nov 05, 2020) + +With this release, Fleet has moved to the new github.com/fleetdm/fleet +repository. Please follow changes and releases there. + +* Add file carving functionality. + +* Add `fleetctl user create` command. + +* Add osquery options editor to admin pages in UI. + +* Add `fleetctl query --pretty` option for pretty-printing query results. + +* Add ability to disable packs with `fleetctl apply`. + +* Improve "Add New Host" dialog to walk the user step-by-step through host enrollment. + +* Improve 500 error page by allowing display of the error. + +* Partial transition of branding away from "Kolide Fleet". + +* Fix an issue with case insensitive enroll secret and node key authentication. + +* Fix an issue with `fleetctl query --quiet` flag not actually suppressing output. + + ## Fleet 3.2.0 (Aug 08, 2020) * Add `stdout` logging plugin. @@ -24,7 +50,7 @@ * Fix cleanup of queries in bad state. This should resolve issues in which users experienced old live queries repeatedly returned to hosts. -* Fix output kind of `fleetctl get options` +* Fix output kind of `fleetctl get options`. ## Fleet 3.1.0 (Aug 06, 2020) diff --git a/LICENSE b/LICENSE index 0b55c3289..7a18e5155 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,14 @@ -MIT License +Copyright (c) 2020-present Fleet Device Management Inc +Portions of this software are licensed as follows: + +* All content residing under the "docs/" directory of this repository, if that directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE". +* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into the software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. + +Copyright (c) 2020-present Fleet Device Management Inc Copyright (c) 2017 Kolide Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/Makefile b/Makefile index 53721f306..61f8dae05 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ BRANCH = $(shell git rev-parse --abbrev-ref HEAD) REVISION = $(shell git rev-parse HEAD) REVSHORT = $(shell git rev-parse --short HEAD) USER = $(shell whoami) -DOCKER_IMAGE_NAME = kolide/fleet +DOCKER_IMAGE_NAME = fleetdm/fleet ifneq ($(OS), Windows_NT) # If on macOS, set the shell to bash explicitly @@ -186,13 +186,13 @@ endif docker-build-release: xp-fleet xp-fleetctl docker build -t "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" . - docker tag "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" kolide/fleet:${VERSION} - docker tag "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" kolide/fleet:latest + docker tag "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" fleetdm/fleet:${VERSION} + docker tag "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" fleetdm/fleet:latest docker-push-release: docker-build-release docker push "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" - docker push kolide/fleet:${VERSION} - docker push kolide/fleet:latest + docker push fleetdm/fleet:${VERSION} + docker push fleetdm/fleet:latest docker-build-circle: @echo ">> building docker image" diff --git a/README.md b/README.md index 8da2bfc38..a4eaef2e2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,18 @@ -# Kolide Fleet [![CircleCI](https://circleci.com/gh/kolide/fleet/tree/master.svg?style=svg)](https://circleci.com/gh/kolide/fleet/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/kolide/fleet)](https://goreportcard.com/report/github.com/kolide/fleet) +:tada: Announcing the transition of Fleet to a new independent entity :tada: -Fleet is the most widely used open-source osquery Fleet manager. Deploying osquery with Fleet enables live queries, and effective management of osquery infrastructure. +Please check out [the blog post](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) to understand what is happening with Fleet and our commitment to improving the product. + +# Fleet [![CircleCI](https://circleci.com/gh/fleetdm/fleet/tree/master.svg?style=svg)](https://circleci.com/gh/fleetdm/fleet/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/fleetdm/fleet)](https://goreportcard.com/report/github.com/fleetdm/fleet) + +Fleet is the most widely used open source osquery manager. Deploying osquery with Fleet enables programmable live queries, streaming logs, and effective management of osquery across 50,000+ servers, containers, and laptops. It's especially useful for talking to multiple devices at the same time. + +Fleet is a Go app. You can run it on your own hardware or deploy it in any cloud. Documentation for Fleet can be found on [GitHub](./docs/README.md). +![banner-fleet-cloud-city](https://user-images.githubusercontent.com/618009/98254443-eaf21100-1f41-11eb-9e2c-63a0545601f3.jpg) + + ## Using Fleet @@ -19,7 +29,7 @@ Documentation for Fleet can be found on [GitHub](./docs/README.md). If you're interested in learning about the `fleetctl` CLI and flexible osquery deployment file format, see the [CLI Documentation](./docs/cli/README.md). -#### Deploying Osquery and Fleet +#### Deploying osquery and Fleet Resources for deploying osquery to hosts, deploying the Fleet server, installing Fleet's infrastructure dependencies, etc. can all be found in the [Infrastructure Documentation](./docs/infrastructure/README.md). @@ -29,34 +39,30 @@ If you are interested in accessing the Fleet REST API in order to programmatical #### The Web Dashboard -Information about using the Kolide web dashboard can be found in the [Dashboard Documentation](./docs/dashboard/README.md). +Information about using the web dashboard can be found in the [Dashboard Documentation](./docs/dashboard/README.md). ## Developing Fleet +Organizations large and small use osquery with Fleet every day to stay secure and compliant. That’s good news, since it means there are lots of other developers and security practitioners talking about Fleet, dreaming up features, and contributing patches. Let’s stop reinventing the wheel and build the future of device management together. + #### Development Documentation -If you're interested in interacting with the Kolide source code, you will find information on modifying and building the code in the [Development Documentation](./docs/development/README.md). +If you're interested in interacting with the Fleet source code, you will find information on modifying and building the code in the [Development Documentation](./docs/development/README.md). -If you have any questions, please create a [GitHub Issue](https://github.com/kolide/fleet/issues/new). +If you have any questions, please create a [GitHub Issue](https://github.com/fleetdm/fleet/issues/new). ## Community #### Chat -Please join us in the #kolide channel on [Osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). +Please join us in the #fleet channel on [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). #### Community Projects -Below are some projects created by Kolide community members. Please submit a pull request if you'd like your project featured. +Below are some projects created by Fleet community members. Please submit a pull request if you'd like your project featured. +- [Kolide](https://kolide.com) is a cloud-hosted, user-driven security SaaS application. To be clear: Kolide ≠ Fleet. Kolide is well-executed, a great commercial tool, and they offer a 30-day free trial. - [davidrecordon/terraform-aws-kolide-fleet](https://github.com/davidrecordon/terraform-aws-kolide-fleet) - Deploy Fleet into AWS using Terraform. - [deeso/fleet-deployment](https://github.com/deeso/fleet-deployment) - Install Fleet on a Ubuntu box. - [gjyoung1974/kolide-fleet-chart](https://github.com/gjyoung1974/kolide-fleet-chart) - Kubernetes Helm chart for deploying Fleet. -## Kolide SaaS - -Looking for the quickest way to try out osquery on your fleet? Not sure which queries to run? Don't want to manage your own data pipeline? - -Try our [osquery SaaS platform](https://kolide.com/?utm_source=oss&utm_medium=readme&utm_campaign=fleet) providing insights, alerting, fleet management and user-driven security tools. We also support advanced aggregation of osquery results for power users. Get started with your 30-day free trial [today](https://k2.kolide.com/signup?utm_source=oss&utm_medium=readme&utm_campaign=fleet). - -[![Rube](./assets/images/rube.png)](https://kolide.com/fleet) diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 000000000..1b5becc45 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/favicons/android-chrome-144x144.png b/assets/favicons/android-chrome-144x144.png deleted file mode 100644 index 02faeccda..000000000 Binary files a/assets/favicons/android-chrome-144x144.png and /dev/null differ diff --git a/assets/favicons/android-chrome-192x192.png b/assets/favicons/android-chrome-192x192.png deleted file mode 100644 index 09db103c1..000000000 Binary files a/assets/favicons/android-chrome-192x192.png and /dev/null differ diff --git a/assets/favicons/android-chrome-256x256.png b/assets/favicons/android-chrome-256x256.png deleted file mode 100644 index 18ef563e1..000000000 Binary files a/assets/favicons/android-chrome-256x256.png and /dev/null differ diff --git a/assets/favicons/android-chrome-36x36.png b/assets/favicons/android-chrome-36x36.png deleted file mode 100644 index effb6fed2..000000000 Binary files a/assets/favicons/android-chrome-36x36.png and /dev/null differ diff --git a/assets/favicons/android-chrome-384x384.png b/assets/favicons/android-chrome-384x384.png deleted file mode 100644 index 5b7669e87..000000000 Binary files a/assets/favicons/android-chrome-384x384.png and /dev/null differ diff --git a/assets/favicons/android-chrome-48x48.png b/assets/favicons/android-chrome-48x48.png deleted file mode 100644 index 7671d2510..000000000 Binary files a/assets/favicons/android-chrome-48x48.png and /dev/null differ diff --git a/assets/favicons/android-chrome-512x512.png b/assets/favicons/android-chrome-512x512.png deleted file mode 100644 index 381f34291..000000000 Binary files a/assets/favicons/android-chrome-512x512.png and /dev/null differ diff --git a/assets/favicons/android-chrome-72x72.png b/assets/favicons/android-chrome-72x72.png deleted file mode 100644 index dc5ba5584..000000000 Binary files a/assets/favicons/android-chrome-72x72.png and /dev/null differ diff --git a/assets/favicons/android-chrome-96x96.png b/assets/favicons/android-chrome-96x96.png deleted file mode 100644 index 8bf4e225f..000000000 Binary files a/assets/favicons/android-chrome-96x96.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-114x114.png b/assets/favicons/apple-touch-icon-114x114.png deleted file mode 100644 index bc74bc1f2..000000000 Binary files a/assets/favicons/apple-touch-icon-114x114.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-120x120.png b/assets/favicons/apple-touch-icon-120x120.png deleted file mode 100644 index 531919567..000000000 Binary files a/assets/favicons/apple-touch-icon-120x120.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-144x144.png b/assets/favicons/apple-touch-icon-144x144.png deleted file mode 100644 index bbe0c6d15..000000000 Binary files a/assets/favicons/apple-touch-icon-144x144.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-152x152.png b/assets/favicons/apple-touch-icon-152x152.png deleted file mode 100644 index a5a5b030e..000000000 Binary files a/assets/favicons/apple-touch-icon-152x152.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-167x167.png b/assets/favicons/apple-touch-icon-167x167.png deleted file mode 100644 index 7c4831685..000000000 Binary files a/assets/favicons/apple-touch-icon-167x167.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-180x180.png b/assets/favicons/apple-touch-icon-180x180.png deleted file mode 100644 index 2c07fce81..000000000 Binary files a/assets/favicons/apple-touch-icon-180x180.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-57x57.png b/assets/favicons/apple-touch-icon-57x57.png deleted file mode 100644 index da794c211..000000000 Binary files a/assets/favicons/apple-touch-icon-57x57.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-60x60.png b/assets/favicons/apple-touch-icon-60x60.png deleted file mode 100644 index aecaabae3..000000000 Binary files a/assets/favicons/apple-touch-icon-60x60.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-72x72.png b/assets/favicons/apple-touch-icon-72x72.png deleted file mode 100644 index 1edc2c339..000000000 Binary files a/assets/favicons/apple-touch-icon-72x72.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-76x76.png b/assets/favicons/apple-touch-icon-76x76.png deleted file mode 100644 index dffda6db4..000000000 Binary files a/assets/favicons/apple-touch-icon-76x76.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon-precomposed.png b/assets/favicons/apple-touch-icon-precomposed.png deleted file mode 100644 index 2c07fce81..000000000 Binary files a/assets/favicons/apple-touch-icon-precomposed.png and /dev/null differ diff --git a/assets/favicons/apple-touch-icon.png b/assets/favicons/apple-touch-icon.png deleted file mode 100644 index 2c07fce81..000000000 Binary files a/assets/favicons/apple-touch-icon.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-1182x2208.png b/assets/favicons/apple-touch-startup-image-1182x2208.png deleted file mode 100644 index 2ec122502..000000000 Binary files a/assets/favicons/apple-touch-startup-image-1182x2208.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-1242x2148.png b/assets/favicons/apple-touch-startup-image-1242x2148.png deleted file mode 100644 index bd09b5e37..000000000 Binary files a/assets/favicons/apple-touch-startup-image-1242x2148.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-1496x2048.png b/assets/favicons/apple-touch-startup-image-1496x2048.png deleted file mode 100644 index 1296ad832..000000000 Binary files a/assets/favicons/apple-touch-startup-image-1496x2048.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-1536x2008.png b/assets/favicons/apple-touch-startup-image-1536x2008.png deleted file mode 100644 index e1b515d41..000000000 Binary files a/assets/favicons/apple-touch-startup-image-1536x2008.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-320x460.png b/assets/favicons/apple-touch-startup-image-320x460.png deleted file mode 100644 index 9c5fb203a..000000000 Binary files a/assets/favicons/apple-touch-startup-image-320x460.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-640x1096.png b/assets/favicons/apple-touch-startup-image-640x1096.png deleted file mode 100644 index f8cd896bb..000000000 Binary files a/assets/favicons/apple-touch-startup-image-640x1096.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-640x920.png b/assets/favicons/apple-touch-startup-image-640x920.png deleted file mode 100644 index deebbac8a..000000000 Binary files a/assets/favicons/apple-touch-startup-image-640x920.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-748x1024.png b/assets/favicons/apple-touch-startup-image-748x1024.png deleted file mode 100644 index a67b92790..000000000 Binary files a/assets/favicons/apple-touch-startup-image-748x1024.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-750x1294.png b/assets/favicons/apple-touch-startup-image-750x1294.png deleted file mode 100644 index 98a7590ed..000000000 Binary files a/assets/favicons/apple-touch-startup-image-750x1294.png and /dev/null differ diff --git a/assets/favicons/apple-touch-startup-image-768x1004.png b/assets/favicons/apple-touch-startup-image-768x1004.png deleted file mode 100644 index e2bc9544f..000000000 Binary files a/assets/favicons/apple-touch-startup-image-768x1004.png and /dev/null differ diff --git a/assets/favicons/browserconfig.xml b/assets/favicons/browserconfig.xml deleted file mode 100644 index 9b9e3d953..000000000 --- a/assets/favicons/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff diff --git a/assets/favicons/favicon-16x16.png b/assets/favicons/favicon-16x16.png deleted file mode 100644 index 31938ad2e..000000000 Binary files a/assets/favicons/favicon-16x16.png and /dev/null differ diff --git a/assets/favicons/favicon-32x32.png b/assets/favicons/favicon-32x32.png deleted file mode 100644 index c27ecd9d8..000000000 Binary files a/assets/favicons/favicon-32x32.png and /dev/null differ diff --git a/assets/favicons/favicon.ico b/assets/favicons/favicon.ico deleted file mode 100644 index 96e8aa319..000000000 Binary files a/assets/favicons/favicon.ico and /dev/null differ diff --git a/assets/favicons/manifest.json b/assets/favicons/manifest.json deleted file mode 100644 index 2415a38be..000000000 --- a/assets/favicons/manifest.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "Kolide Fleet", - "short_name": "Fleet", - "dir": "auto", - "lang": "en-US", - "display": "standalone", - "orientation": "any", - "start_url": "/", - "background_color": "#fff", - "icons": [ - { - "src": "android-chrome-36x36.png", - "sizes": "36x36", - "type": "image/png" - }, - { - "src": "android-chrome-48x48.png", - "sizes": "48x48", - "type": "image/png" - }, - { - "src": "android-chrome-72x72.png", - "sizes": "72x72", - "type": "image/png" - }, - { - "src": "android-chrome-96x96.png", - "sizes": "96x96", - "type": "image/png" - }, - { - "src": "android-chrome-144x144.png", - "sizes": "144x144", - "type": "image/png" - }, - { - "src": "android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - }, - { - "src": "android-chrome-384x384.png", - "sizes": "384x384", - "type": "image/png" - }, - { - "src": "android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} diff --git a/assets/favicons/mstile-144x144.png b/assets/favicons/mstile-144x144.png deleted file mode 100644 index 02faeccda..000000000 Binary files a/assets/favicons/mstile-144x144.png and /dev/null differ diff --git a/assets/favicons/mstile-150x150.png b/assets/favicons/mstile-150x150.png deleted file mode 100644 index 6a771f322..000000000 Binary files a/assets/favicons/mstile-150x150.png and /dev/null differ diff --git a/assets/favicons/mstile-310x150.png b/assets/favicons/mstile-310x150.png deleted file mode 100644 index f60ba5662..000000000 Binary files a/assets/favicons/mstile-310x150.png and /dev/null differ diff --git a/assets/favicons/mstile-310x310.png b/assets/favicons/mstile-310x310.png deleted file mode 100644 index 1049872a4..000000000 Binary files a/assets/favicons/mstile-310x310.png and /dev/null differ diff --git a/assets/favicons/mstile-70x70.png b/assets/favicons/mstile-70x70.png deleted file mode 100644 index d26c6fca8..000000000 Binary files a/assets/favicons/mstile-70x70.png and /dev/null differ diff --git a/assets/images/kolide-logo-color@2x.png b/assets/images/kolide-logo-color@2x.png index 4cce10d70..0865f1d70 100644 Binary files a/assets/images/kolide-logo-color@2x.png and b/assets/images/kolide-logo-color@2x.png differ diff --git a/assets/images/kolide-white@2x.png b/assets/images/kolide-white@2x.png index b17813f5c..a03181cf9 100644 Binary files a/assets/images/kolide-white@2x.png and b/assets/images/kolide-white@2x.png differ diff --git a/cmd/fleet/prepare.go b/cmd/fleet/prepare.go index 101b45097..fc31fa1d2 100644 --- a/cmd/fleet/prepare.go +++ b/cmd/fleet/prepare.go @@ -115,7 +115,7 @@ To setup Fleet infrastructure, use one of the available commands. initFatal(err, "creating service") } - _, err = svc.NewAdminCreatedUser(context.Background(), admin) + _, err = svc.CreateUser(context.Background(), admin) if err != nil { initFatal(err, "saving new user") } diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index e14cad434..d5530f357 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -191,6 +191,7 @@ the way that the Fleet server works. for { ds.CleanupDistributedQueryCampaigns(time.Now()) ds.CleanupIncomingHosts(time.Now()) + ds.CleanupCarves(time.Now()) <-ticker.C } }() diff --git a/cmd/fleetctl/fleetctl.go b/cmd/fleetctl/fleetctl.go index 3eb4a8a73..cb4a0d48f 100644 --- a/cmd/fleetctl/fleetctl.go +++ b/cmd/fleetctl/fleetctl.go @@ -39,6 +39,7 @@ func main() { }, convertCommand(), goqueryCommand(), + userCommand(), } app.RunAndExitOnError() diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 9818d6b13..833fd94ce 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "io" "github.com/ghodss/yaml" "github.com/kolide/fleet/server/kolide" @@ -17,6 +18,9 @@ const ( yamlFlagName = "yaml" jsonFlagName = "json" withQueriesFlagName = "with-queries" + expiredFlagName = "expired" + outfileFlagName = "outfile" + stdoutFlagName = "stdout" ) type specGeneric struct { @@ -210,6 +214,8 @@ func getCommand() cli.Command { getHostsCommand(), getEnrollSecretCommand(), getAppConfigCommand(), + getCarveCommand(), + getCarvesCommand(), }, } } @@ -241,7 +247,7 @@ func getQueriesCommand() cli.Command { } if len(queries) == 0 { - fmt.Println("no queries found") + fmt.Println("No queries found") return nil } @@ -364,7 +370,7 @@ func getPacksCommand() cli.Command { } if len(packs) == 0 { - fmt.Println("no packs found") + fmt.Println("No packs found") return nil } @@ -439,7 +445,7 @@ func getLabelsCommand() cli.Command { } if len(labels) == 0 { - fmt.Println("no labels found") + fmt.Println("No labels found") return nil } @@ -596,7 +602,7 @@ func getHostsCommand() cli.Command { } if len(hosts) == 0 { - fmt.Println("no hosts found") + fmt.Println("No hosts found") return nil } @@ -643,3 +649,141 @@ func getHostsCommand() cli.Command { }, } } + +func getCarvesCommand() cli.Command { + return cli.Command{ + Name: "carves", + Usage: "Retrieve the file carving sessions", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.BoolFlag{ + Name: expiredFlagName, + Usage: "Include expired carves", + }, + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + expired := c.Bool(expiredFlagName) + + carves, err := fleet.ListCarves(kolide.CarveListOptions{Expired: expired}) + if err != nil { + return err + } + + if len(carves) == 0 { + fmt.Println("No carves found") + return nil + } + + data := [][]string{} + for _, c := range carves { + completion := fmt.Sprintf( + "%d%%", + int64((float64(c.MaxBlock+1)/float64(c.BlockCount))*100), + ) + if c.Expired { + completion = "Expired" + } + + data = append(data, []string{ + strconv.FormatInt(c.ID, 10), + c.CreatedAt.Local().String(), + c.RequestId, + strconv.FormatInt(c.CarveSize, 10), + completion, + }) + } + + table := defaultTable() + table.SetHeader([]string{"id", "created_at", "request_id", "carve_size", "completion"}) + table.AppendBulk(data) + table.Render() + + return nil + }, + } +} + + +func getCarveCommand() cli.Command { + return cli.Command{ + Name: "carve", + Usage: "Retrieve details for a carve by ID", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.BoolFlag{ + Name: stdoutFlagName, + Usage: "Print carve contents to stdout", + }, + cli.StringFlag{ + Name: outfileFlagName, + Usage: "Download carve contents to specified file path", + }, + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + idString := c.Args().First() + + if idString == "" { + return errors.Errorf("must provide carve ID as first argument") + } + + id, err := strconv.ParseInt(idString, 10, 64) + if err != nil { + return errors.Wrap(err, "unable to parse carve ID as int") + } + + outFile := c.String(outfileFlagName) + stdout := c.Bool(stdoutFlagName) + + if stdout && outFile != "" { + return errors.Errorf("-stdout and -outfile must not be specified together") + } + + if stdout || outFile != "" { + out := os.Stdout + if outFile != "" { + f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return errors.Wrap(err, "open out file") + } + defer f.Close() + out = f + } + + reader, err := fleet.DownloadCarve(id) + if err != nil { + return err + } + + if _, err := io.Copy(out, reader); err != nil { + return errors.Wrap(err, "download carve contents") + } + + return nil + } + + carve, err := fleet.GetCarve(id) + if err != nil { + return err + } + + if err := printYaml(carve); err != nil { + return errors.Wrap(err, "print carve yaml") + } + + return nil + }, + } +} + diff --git a/cmd/fleetctl/query.go b/cmd/fleetctl/query.go index ca3c65159..709f0a51a 100644 --- a/cmd/fleetctl/query.go +++ b/cmd/fleetctl/query.go @@ -1,26 +1,22 @@ package main import ( - "encoding/json" "errors" "fmt" "os" "strings" "time" + "io/ioutil" "github.com/briandowns/spinner" "github.com/urfave/cli" ) -type resultOutput struct { - HostIdentifier string `json:"host"` - Rows []map[string]string `json:"rows"` -} func queryCommand() cli.Command { var ( flHosts, flLabels, flQuery, flQueryName string - flDebug, flQuiet, flExit bool + flDebug, flQuiet, flExit, flPretty bool flTimeout time.Duration ) return cli.Command{ @@ -76,6 +72,12 @@ func queryCommand() cli.Command { Destination: &flDebug, Usage: "Whether or not to enable debug logging", }, + cli.BoolFlag{ + Name: "pretty", + EnvVar: "PRETTY", + Destination: &flPretty, + Usage: "Enable pretty-printing", + }, cli.DurationFlag{ Name: "timeout", EnvVar: "TIMEOUT", @@ -109,6 +111,13 @@ func queryCommand() cli.Command { return fmt.Errorf("Query must be specified with --query or --query-name") } + var output outputWriter + if flPretty { + output = newPrettyWriter() + } else { + output = newJsonWriter() + } + hosts := strings.Split(flHosts, ",") labels := strings.Split(flLabels, ",") @@ -124,9 +133,10 @@ func queryCommand() cli.Command { // https://godoc.org/github.com/briandowns/spinner#pkg-variables s := spinner.New(spinner.CharSets[24], 200*time.Millisecond) s.Writer = os.Stderr - if !flQuiet { - s.Start() + if flQuiet { + s.Writer = ioutil.Discard } + s.Start() var timeoutChan <-chan time.Time if flTimeout > 0 { @@ -142,11 +152,12 @@ func queryCommand() cli.Command { select { // Print a result case hostResult := <-res.Results(): - out := resultOutput{hostResult.Host.HostName, hostResult.Rows} s.Stop() - if err := json.NewEncoder(os.Stdout).Encode(out); err != nil { - fmt.Fprintf(os.Stderr, "Error writing output: %s\n", err) + + if err := output.WriteResult(hostResult); err != nil { + fmt.Fprintf(os.Stderr, "Error writing result: %s\n", err) } + s.Start() // Print an error @@ -176,9 +187,7 @@ func queryCommand() cli.Command { } msg := fmt.Sprintf(" %.f%% responded (%.f%% online) | %d/%d targeted hosts (%d/%d online)", percentTotal, percentOnline, responded, total, responded, online) - if !flQuiet { - s.Suffix = msg - } + s.Suffix = msg if total == responded && status != nil { s.Stop() if !flQuiet { diff --git a/cmd/fleetctl/query_output.go b/cmd/fleetctl/query_output.go new file mode 100644 index 000000000..135cb16b8 --- /dev/null +++ b/cmd/fleetctl/query_output.go @@ -0,0 +1,83 @@ +package main + +import ( + "sort" + "encoding/json" + "os" + + "github.com/olekukonko/tablewriter" + "github.com/gosuri/uilive" + "github.com/kolide/fleet/server/kolide" +) + +type outputWriter interface{ + WriteResult(res kolide.DistributedQueryResult) error +} + +type resultOutput struct { + HostIdentifier string `json:"host"` + Rows []map[string]string `json:"rows"` +} + +type jsonWriter struct {} + +func newJsonWriter() *jsonWriter { + return &jsonWriter{} +} + +func (w *jsonWriter) WriteResult(res kolide.DistributedQueryResult) error { + out := resultOutput{res.Host.HostName, res.Rows} + return json.NewEncoder(os.Stdout).Encode(out) +} + +type prettyWriter struct { + results []kolide.DistributedQueryResult + columns map[string]bool + writer *uilive.Writer +} + +func newPrettyWriter() *prettyWriter{ + return &prettyWriter{ + columns: make(map[string]bool), + writer: uilive.New(), + } +} + +func (w *prettyWriter) WriteResult(res kolide.DistributedQueryResult) error { + w.results = append(w.results, res) + + // Recompute columns + for _, row := range res.Rows { + delete(row, "host_hostname") + for col := range row { + w.columns[col] = true + } + } + + columns := []string{} + for col := range w.columns { + columns = append(columns, col) + } + sort.Strings(columns) + + table := tablewriter.NewWriter(w.writer.Newline()) + table.SetRowLine(true) + table.SetHeader(append([]string{"hostname"}, columns...)) + + // Extract columns from the results in the appropriate order + for _, res := range w.results { + for _, row := range res.Rows { + cols := []string{res.Host.HostName} + for _, col := range columns { + cols = append(cols, row[col]) + } + table.Append(cols) + } + } + table.Render() + + // Actually write the output + w.writer.Flush() + + return nil +} diff --git a/cmd/fleetctl/user.go b/cmd/fleetctl/user.go new file mode 100644 index 000000000..d763c40bf --- /dev/null +++ b/cmd/fleetctl/user.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "fmt" + "os" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" +) + +const ( + adminFlagName = "admin" + usernameFlagName = "username" + passwordFlagName = "password" + emailFlagName = "email" + ssoFlagName = "sso" +) + +func userCommand() cli.Command { + return cli.Command{ + Name: "user", + Usage: "Manage Fleet users", + Subcommands: []cli.Command{ + createUserCommand(), + }, + } +} + +func createUserCommand() cli.Command { + return cli.Command{ + Name: "create", + Usage: "Create a new user", + UsageText: `This command will create a new user in Fleet. By default, the user will authenticate with a password and will not have admin privileges. + + If a password is required and not provided by flag, the command will prompt for password input through stdin.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: usernameFlagName, + Usage: "Username for new user (required)", + Required: true, + }, + cli.StringFlag{ + Name: emailFlagName, + Usage: "Email for new user (required)", + Required: true, + }, + cli.StringFlag{ + Name: passwordFlagName, + Usage: "Password for new user", + }, + cli.BoolFlag{ + Name: adminFlagName, + Usage: "Grant admin privileges to created user (default false)", + }, + cli.BoolFlag{ + Name: ssoFlagName, + Usage: "Enable user login via SSO (default false)", + }, + configFlag(), + contextFlag(), + yamlFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + username := c.String(usernameFlagName) + password := c.String(passwordFlagName) + email := c.String(emailFlagName) + admin := c.Bool(adminFlagName) + sso := c.Bool(ssoFlagName) + + if sso && len(password) > 0 { + return fmt.Errorf("Password may not be provided for SSO users.") + } + if !sso && len(password) == 0 { + fmt.Print("Enter password for user: ") + passBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return errors.Wrap(err, "Failed to read password") + } + if len(passBytes) == 0 { + return fmt.Errorf("Password may not be empty.") + } + + fmt.Print("Enter password for user (confirm): ") + confBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return errors.Wrap(err, "Failed to read confirmation") + } + + if !bytes.Equal(passBytes, confBytes) { + return fmt.Errorf("Confirmation does not match") + } + + password = string(passBytes) + } + + // Only set the password reset flag if SSO is not enabled. Otherwise + // the user will be stuck in a bad state and not be able to log in. + force_reset := !sso + err = fleet.CreateUser(kolide.UserPayload{ + Username: &username, + Password: &password, + Email: &email, + Admin: &admin, + SSOEnabled: &sso, + AdminForcedPasswordReset: &force_reset, + }) + if err != nil { + return errors.Wrap(err, "Failed to create user") + } + + return nil + }, + } +} diff --git a/docs/README.md b/docs/README.md index e267c0391..738155a26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,4 +9,4 @@ Welcome to the documentation for the Kolide Fleet osquery fleet manager. - Architecturally significant decisions are documented in the [Architecture Documentation](./architecture/README.md). - Finally, if you're interested in interacting with the Fleet source code, you will find information on modifying and building the code in the [Development Documentation](./development/README.md). -If you have any questions, please don't hesitate to [File a GitHub issue](https://github.com/kolide/fleet/issues) or [join us on Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). You can find us in the `#kolide` channel. +If you have any questions, please don't hesitate to [File a GitHub issue](https://github.com/fleetdm/fleet/issues) or [join us on Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). You can find us in the `#fleet` channel. diff --git a/docs/api/README.md b/docs/api/README.md index 1881d2ef3..703f80b60 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -10,7 +10,7 @@ Kolide Fleet is powered by a Go API server which serves three types of endpoints Only osquery agents should interact with the osquery API, but we'd like to support the eventual use of the Fleet API extensively. The API is not very well documented at all right now, but we have plans to: - Generate and publish detailed documentation via a tool built using [test2doc](https://github.com/adams-sarah/test2doc) (or similar). -- Release a JavaScript Fleet API client library (which would be derived from the [current](https://github.com/kolide/fleet/blob/master/frontend/kolide/index.js) JavaScript API client). +- Release a JavaScript Fleet API client library (which would be derived from the [current](https://github.com/fleetdm/fleet/blob/master/frontend/kolide/index.js) JavaScript API client). - Commit to a stable, standardized API format. ## Fleetctl @@ -36,4 +36,4 @@ Each set of objects follows a similar REST access pattern. Queries, packs, scheduled queries, labels, invites, users, sessions all behave this way. Some objects, like invites, have additional HTTP methods for additional functionality. Some objects, such as scheduled queries, are merely a relationship between two other objects (in this case, a query and a pack) with some details attached. -All of these objects are put together and distributed to the appropriate osquery agents at the appropriate time. At this time, the best source of truth for the API is the [HTTP handler file](https://github.com/kolide/fleet/blob/master/server/service/handler.go) in the Go application. The REST API is exposed via a transport layer on top of an RPC service which is implemented using a micro-service library called [Go Kit](https://github.com/go-kit/kit). If using the Fleet API is important to you right now, being familiar with Go Kit would definitely be helpful. +All of these objects are put together and distributed to the appropriate osquery agents at the appropriate time. At this time, the best source of truth for the API is the [HTTP handler file](https://github.com/fleetdm/fleet/blob/master/server/service/handler.go) in the Go application. The REST API is exposed via a transport layer on top of an RPC service which is implemented using a micro-service library called [Go Kit](https://github.com/go-kit/kit). If using the Fleet API is important to you right now, being familiar with Go Kit would definitely be helpful. diff --git a/docs/development/building-the-code.md b/docs/development/building-the-code.md index 1e959c0c8..5b9bc589c 100644 --- a/docs/development/building-the-code.md +++ b/docs/development/building-the-code.md @@ -3,20 +3,25 @@ Building The Code ## Building the Code -Checkout this repository to `$GOPATH/src/github.com/kolide/fleet`. If you're new to Go and you don't know about `$GOPATH`, then check out the repo to `$HOME/go/src/github.com/kolide/fleet`. You will also need to install Go (1.9 or greater). - -* [Go Documentation: Workspaces](https://golang.org/doc/code.html#Workspaces) +Clone this repository. To setup a working local development environment, you must install the following minimum toolset: * [Go](https://golang.org/dl/) (1.9 or greater) * [Node.js](https://nodejs.org/en/download/current/) and [Yarn](https://yarnpkg.com/en/docs/install) -* [GNU Make](https://www.gnu.org/software/make/) +* [GNU Make](https://www.gnu.org/software/make/) (probably already installed if you're on macOS/Linux) * [Docker](https://www.docker.com/products/overview#/install_the_platform) -If you're using MacOS or Linux, Make should be installed by default. If you are using Windows, you will need to install it separately. +> #### New to the Go language? +> +> After installing Go, your $GOPATH will probably need a little freshening up. To take care of this automatically every time a new terminal is opened, add this to your shell startup script (`~/.profile`): +> ```bash +> # Allow go-bindata and other Go stuff to work properly (e.g. for Fleet/osquery) +> # More info: https://golang.org/doc/gopath_code.html#GOPATH +> export PATH=$PATH:$(go env GOPATH)/bin +> ``` -Once you have those minimum requirements, you will need to install Fleet's dependent libraries. To do this, run the following from the root of the repository: +Once you have those minimum requirements, you will need to install Fleet's dependencies. To do this, run the following from the root of the repository: ``` make deps diff --git a/docs/development/faq.md b/docs/development/faq.md index 14bf186e6..d79333575 100644 --- a/docs/development/faq.md +++ b/docs/development/faq.md @@ -19,4 +19,4 @@ server/kolide/emails.go:90:23: undefined: Asset make: *** [fleet] Error 2 ``` -If you get an `undefined: Asset` error it is likely because you did not run `make generate` before `make build`. See [Building the Code](https://github.com/kolide/fleet/blob/master/docs/development/building-the-code.md) for additional documentation on compiling the `fleet` binary. +If you get an `undefined: Asset` error it is likely because you did not run `make generate` before `make build`. See [Building the Code](https://github.com/fleetdm/fleet/blob/master/docs/development/building-the-code.md) for additional documentation on compiling the `fleet` binary. diff --git a/docs/development/linux.md b/docs/development/linux.md index 00134ac5c..0446bc440 100644 --- a/docs/development/linux.md +++ b/docs/development/linux.md @@ -42,8 +42,7 @@ rm -rf tmp ### Clone and build depenencies ``` -mkdir -p ~/go/src/github.com/kolide/ -git clone https://github.com/kolide/fleet.git +git clone https://github.com/fleetdm/fleet.git cd fleet make deps make generate diff --git a/docs/development/release.md b/docs/development/release.md index 06fe123ec..a1d238555 100644 --- a/docs/development/release.md +++ b/docs/development/release.md @@ -16,7 +16,7 @@ git push origin make binary-bundle ``` -4. Create a new release on the [GitHub releases page](https://github.com/kolide/fleet/releases). Select the newly pushed tag (GitHub should say "Existing tag"). Use the version number as the release title. Use the below template for the release description (replace items in <> with the appropriate values): +4. Create a new release on the [GitHub releases page](https://github.com/fleetdm/fleet/releases). Select the newly pushed tag (GitHub should say "Existing tag"). Use the version number as the release title. Use the below template for the release description (replace items in <> with the appropriate values): ```` ### Changes @@ -25,11 +25,11 @@ make binary-bundle ### Upgrading -Please visit our [update guide](https://github.com/kolide/fleet/blob/master/docs/infrastructure/updating-fleet.md) for upgrade instructions. +Please visit our [update guide](https://github.com/fleetdm/fleet/blob/master/docs/infrastructure/updating-fleet.md) for upgrade instructions. ### Documentation -Documentation for this release can be found at https://github.com/kolide/fleet/blob//docs/README.md +Documentation for this release can be found at https://github.com/fleetdm/fleet/blob//docs/README.md ### Binary Checksum @@ -48,4 +48,4 @@ Upload the `fleet.zip` binary bundle and click "Publish Release". make docker-push-release ``` -6. Announce the release in the #kolide channel of [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). +6. Announce the release in the #fleet channel of [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md index 09a482cb1..4f53fbd5b 100644 --- a/docs/infrastructure/README.md +++ b/docs/infrastructure/README.md @@ -23,6 +23,10 @@ For more information, you can also read the [Configuring The Fleet Binary](./con Fleet allows users to schedule queries, curate packs, and generate a lot of osquery logs. For more information on how you can access these logs as well as examples on what you can do with them, see the [Working With Osquery Logs](./working-with-osquery-logs.md) documentation. +## File Carving + +Learn how to work with the osquery file carving functionality to extract file contents in the [File Carving](./file-carving.md) documentation. + ## Troubleshooting & FAQ Check out the [Frequently Asked Questions](./faq.md), which include troubleshooting steps for the most common issues experience by Fleet users. diff --git a/docs/infrastructure/adding-hosts-to-fleet.md b/docs/infrastructure/adding-hosts-to-fleet.md index 191ad264d..284dc8c81 100644 --- a/docs/infrastructure/adding-hosts-to-fleet.md +++ b/docs/infrastructure/adding-hosts-to-fleet.md @@ -12,25 +12,40 @@ If you'd like to use the native osqueryd binaries to connect to Fleet, this is e ## Kolide Osquery Launcher -We provide compiled releases of the launcher for all supported platforms. Those can be found [here](https://github.com/kolide/launcher/releases). But if you’d like to compile from source, the instructions are [here](https://github.com/kolide/fleet/tree/master/docs/development). +We provide compiled releases of the launcher for all supported platforms. Those can be found [here](https://github.com/kolide/launcher/releases). But if you’d like to compile from source, the instructions are [here](https://github.com/fleetdm/fleet/tree/master/docs/development). #### Connecting a single Launcher to Fleet To directly execute the launcher binary without having to mess with packages, invoke the binary with just a few flags: -- `--hostname`: the hostname of the gRPC server for your environment -- `--root_directory`: the location of the local database, pidfiles, etc. +- `--hostname`: the hostname of Fleet (aka the gRPC server for your environment) +- `--root_directory`: the location for osquery's local database, pidfiles, etc. - `--enroll_secret`: the enroll secret to authenticate hosts with Fleet (retrieve from Fleet UI or `fleetctl get enroll_secret`) ``` +mkdir .osquery ./build/launcher \ --hostname=fleet.acme.net:443 \ - --root_directory=$(mktemp -d) \ + --root_directory=.osquery \ --enroll_secret=32IeN3QLgckHUmMD3iW40kyLdNJcGzP5 ``` -You may also need to define the `--insecure` and/or `--insecure_grpc` flag. If you're running Fleet locally, include `--insecure` because your TLS certificate will not be signed by a valid CA. +You may also need to define the `--insecure` and/or `--insecure_grpc` flag. + + + + + +If you're running Fleet locally, include `--insecure` because your TLS certificate will not be signed by a valid CA: +``` +mkdir .osquery +./build/launcher \ + --hostname=localhost:8412 \ + --root_directory=.osquery \ + --enroll_secret=32IeN3QLgckHUmMD3iW40kyLdNJcGzP5 + --insecure +``` #### Generating packages diff --git a/docs/infrastructure/faq.md b/docs/infrastructure/faq.md index 0f8de5ce3..e0fab1d57 100644 --- a/docs/infrastructure/faq.md +++ b/docs/infrastructure/faq.md @@ -78,6 +78,6 @@ Kolide does not host a SaaS version of Fleet. We offer [Kolide Cloud](https://ko ## How do I get support for working with Fleet? -For bug reports, please use the [Github issue tracker](https://github.com/kolide/fleet/issues). +For bug reports, please use the [Github issue tracker](https://github.com/fleetdm/fleet/issues). -For questions and discussion, please join us in the #kolide channel of [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). +For questions and discussion, please join us in the #fleet channel of [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). diff --git a/docs/infrastructure/file-carving.md b/docs/infrastructure/file-carving.md new file mode 100644 index 000000000..fe1fb7698 --- /dev/null +++ b/docs/infrastructure/file-carving.md @@ -0,0 +1,88 @@ +# File Carving with Fleet + +Fleet supports osquery's file carving functionality as of Fleet 3.3.0. This allows the Fleet server to request files (and sets of files) from osquery agents, returning the full contents to Fleet. + +## Configuration + +Given a working flagfile for connecting osquery agents to Fleet, add the following flags to enable carving: + +``` +--disable_carver=false +--carver_start_endpoint=/api/v1/osquery/carve/begin +--carver_continue_endpoint=/api/v1/osquery/carve/block +--carver_block_size=2000000 +``` + +The default flagfile provided in the "Add New Host" dialog also includes this configuration. + +### Carver Block Size + +The `carver_block_size` flag should be configured in osquery. 2MB (`2000000`) is a good starting value. + +The configured value must be less than the value of `max_allowed_packet` in the MySQL connection, allowing for some overhead. The default for MySQL 5.7 is 4MB and for MySQL 8 it is 64MB. + +Using a smaller value for `carver_block_size` will lead to more HTTP requests during the carving process, resulting in longer carve times and higher load on the Fleet server. If the value is too high, HTTP requests may run long enough to cause server timeouts. + +### Compression + +Compression of the carve contents can be enabled with the `carver_compression` flag in osquery. When used, the carve results will be compressed with [Zstandard](https://facebook.github.io/zstd/) compression. + +## Usage + +File carves are initiated with osquery queries. Issue a query to the `carves` table, providing `carve = 1` along with the desired path(s) as constraints. + +For example, to extract the `/etc/hosts` file on a host with hostname `mac-workstation`: + +``` +fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path = "/etc/hosts"' +``` + +The standard osquery file globbing syntax is also supported to carve entire directories or more: +``` +fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path LIKE "/etc/%%"' +``` + +### Retrieving Carves + +List the non-expired (see below) carves with `fleetctl get carves`. Note that carves will not be available through this command until osquery checks in to the Fleet server with the first of the carve contents. This can take some time from initiation of the carve. + +To also retrieve expired carves, use `fleetctl get carves --expired`. + +Contents of carves are returned as .tar archives, and compressed if that option is configured. + +To download the contents of a carve with ID 3, use + +``` +fleetctl get carve 3 --outfile carve.tar +``` + +It can also be useful to pipe the results directly into the tar command for unarchiving: + +``` +fleetctl get carve 3 --stdout | tar -x +``` + +### Expiration + +Carve contents remain available for 24 hours after the first data is provided from the osquery client. After this time, the carve contents are cleaned from the database and the carve is marked as "expired". + +## Troubleshooting + +### Check carve status in osquery + +Osquery can report on the status of carves through queries to the `carves` table. + +The details provided by + +``` +fleetctl query --labels 'All Hosts' --query 'SELECT * FROM carves' +``` + +can be helpful to debug carving problems. + +### Ensure `carver_block_size` is set appropriately + +This value must be less than the `max_allowed_packet` setting in MySQL. If it is too large, MySQL will reject the writes. + +The value must be small enough that HTTP requests do not time out. + diff --git a/docs/infrastructure/fleet-on-centos.md b/docs/infrastructure/fleet-on-centos.md index 850339314..11dff320a 100644 --- a/docs/infrastructure/fleet-on-centos.md +++ b/docs/infrastructure/fleet-on-centos.md @@ -21,7 +21,7 @@ $ vagrant ssh To install Fleet, run the following: ``` -$ wget https://github.com/kolide/fleet/releases/latest/download/fleet.zip +$ wget https://github.com/fleetdm/fleet/releases/latest/download/fleet.zip $ unzip fleet.zip 'linux/*' -d fleet $ sudo cp fleet/linux/fleet* /usr/bin/ ``` diff --git a/docs/infrastructure/fleet-on-ubuntu.md b/docs/infrastructure/fleet-on-ubuntu.md index 6f9d9904f..747779590 100644 --- a/docs/infrastructure/fleet-on-ubuntu.md +++ b/docs/infrastructure/fleet-on-ubuntu.md @@ -21,7 +21,7 @@ $ vagrant ssh To install Fleet, run the following: ``` -$ wget https://github.com/kolide/fleet/releases/latest/download/fleet.zip +$ wget https://github.com/fleetdm/fleet/releases/latest/download/fleet.zip $ unzip fleet.zip 'linux/*' -d fleet $ sudo cp fleet/linux/fleet /usr/bin/fleet $ sudo cp fleet/linux/fleetctl /usr/bin/fleetctl diff --git a/docs/infrastructure/installing-fleet.md b/docs/infrastructure/installing-fleet.md index 33a87a380..f01b957a6 100644 --- a/docs/infrastructure/installing-fleet.md +++ b/docs/infrastructure/installing-fleet.md @@ -18,7 +18,7 @@ Because everyone's infrastructure is different, there are a multiple options ava Pull the latest Fleet docker image: ``` -docker pull kolide/fleet +docker pull fleetdm/fleet ``` For more information on using Fleet, refer to the [Configuring The Fleet Binary](./configuring-the-fleet-binary.md) documentation. @@ -28,7 +28,7 @@ For more information on using Fleet, refer to the [Configuring The Fleet Binary] Download the latest raw Fleet binaries: ``` -curl -LO https://github.com/kolide/fleet/releases/latest/download/fleet.zip +curl -LO https://github.com/fleetdm/fleet/releases/latest/download/fleet.zip ``` Unzip the binaries for your platform: diff --git a/docs/infrastructure/owasp-top-10.md b/docs/infrastructure/owasp-top-10.md index 06e1cdd22..b66741069 100644 --- a/docs/infrastructure/owasp-top-10.md +++ b/docs/infrastructure/owasp-top-10.md @@ -16,7 +16,7 @@ The Fleet community follows best practices when coding. Here are some of the wa - Fleet supports SAML auth which means that it can be configured such that it never sees passwords. - Passwords are never stored in plaintext in the database. We store a `bcrypt`ed hash of the password along with a randomly generated salt. The `bcrypt` iteration count and salt key size are admin-configurable. #### Authentication tokens -- The size and expiration time of session tokens is admin-configurable. See [https://github.com/kolide/fleet/blob/master/docs/infrastructure/configuring-the-fleet-binary.md#session_duration](https://github.com/kolide/fleet/blob/master/docs/infrastructure/configuring-the-fleet-binary.md#session_duration). +- The size and expiration time of session tokens is admin-configurable. See [https://github.com/fleetdm/fleet/blob/master/docs/infrastructure/configuring-the-fleet-binary.md#session_duration](https://github.com/fleetdm/fleet/blob/master/docs/infrastructure/configuring-the-fleet-binary.md#session_duration). - It is possible to revoke all session tokens for a user by forcing a password reset. @@ -24,7 +24,7 @@ The Fleet community follows best practices when coding. Here are some of the wa - By default, all traffic between user clients (such as the web browser and fleetctl) and the Fleet server is encrypted with TLS. By default, all traffic between osqueryd clients and the Fleet server is encrypted with TLS. Fleet does not encrypt any data at rest (*however a user could separately configure encryption for the MySQL database and logs that Fleet writes*). ### Broken access controls – how restrictions on what authorized users are allowed to do/access are enforced. -- Each session is associated with a viewer context that is used to determine the access granted to that user. Access controls can easily be applied as middleware in the routing table, so the access to a route is clearly defined in the same place where the route is attached to the server see [https://github.com/kolide/fleet/blob/master/server/service/handler.go#L114-L189](https://github.com/kolide/fleet/blob/master/server/service/handler.go#L114-L189). +- Each session is associated with a viewer context that is used to determine the access granted to that user. Access controls can easily be applied as middleware in the routing table, so the access to a route is clearly defined in the same place where the route is attached to the server see [https://github.com/fleetdm/fleet/blob/master/server/service/handler.go#L114-L189](https://github.com/fleetdm/fleet/blob/master/server/service/handler.go#L114-L189). ### Cross-site scripting – ensure an attacker can’t execute scripts in the user’s browser - We render the frontend with React and benefit from built-in XSS protection in React's rendering. This is not sufficient to prevent all XSS, so we also follow additional best practices as discussed in [https://stackoverflow.com/a/51852579/491710](https://stackoverflow.com/a/51852579/491710). diff --git a/docs/infrastructure/updating-fleet.md b/docs/infrastructure/updating-fleet.md index a8cfe0c1e..344d2da85 100644 --- a/docs/infrastructure/updating-fleet.md +++ b/docs/infrastructure/updating-fleet.md @@ -19,7 +19,7 @@ Follow the binary update instructions corresponding to the original installation Download the latest raw Fleet binaries: ``` -curl -O https://github.com/kolide/fleet/releases/latest/download/fleet.zip +curl -O https://github.com/fleetdm/fleet/releases/latest/download/fleet.zip ``` Unzip the binaries for your platform: @@ -41,7 +41,7 @@ Replace the existing Fleet binary with the newly unzipped binary. Pull the latest Fleet docker image: ``` -docker pull kolide/fleet +docker pull fleetdm/fleet ``` ## Running database migrations diff --git a/frontend/components/YamlAce/YamlAce.jsx b/frontend/components/YamlAce/YamlAce.jsx new file mode 100644 index 000000000..b780013b7 --- /dev/null +++ b/frontend/components/YamlAce/YamlAce.jsx @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AceEditor from 'react-ace'; +import classnames from 'classnames'; + +import 'ace-builds/src-noconflict/mode-yaml'; + +const baseClass = 'yaml-ace'; + +class YamlAce extends Component { + static propTypes = { + error: PropTypes.string, + label: PropTypes.string, + name: PropTypes.string, + onChange: PropTypes.func.isRequired, + value: PropTypes.string, + wrapperClassName: PropTypes.string, + } + + renderLabel = () => { + const { error, label } = this.props; + + const labelClassName = classnames( + `${baseClass}__label`, + { [`${baseClass}__label--error`]: error }, + ); + + return ( +

{error || label}

+ ); + } + + render() { + const { + label, + name, + onChange, + value, + error, + wrapperClassName, + } = this.props; + + const { renderLabel } = this; + + const wrapperClass = classnames(wrapperClassName, { + [`${baseClass}__wrapper--error`]: error, + }); + + return ( +
+ {renderLabel()} + +
+ ); + } +} + +export default YamlAce; diff --git a/frontend/components/YamlAce/_styles.scss b/frontend/components/YamlAce/_styles.scss new file mode 100644 index 000000000..c92b247e9 --- /dev/null +++ b/frontend/components/YamlAce/_styles.scss @@ -0,0 +1,44 @@ +.yaml-ace { + &__label { + font-size: 16px; + font-weight: $bold; + font-style: normal; + font-stretch: normal; + letter-spacing: -0.5px; + color: $text-dark; + display: block; + margin-bottom: 4px; + min-height: 25px; + + &--error { + color: $alert; + } + } + + &__wrapper { + &--error { + .ace-kolide { + border: 1px solid $alert; + } + } + } + + // Added to remove the "popping" effect when the editor first loads. + min-height: 408px; + + .ace_gutter-layer { + min-height: 408px; + } + + .ace_line { + min-height: 24px; + } + + .ace_gutter-cell { + min-height: 24px; + } + + .ace_fold-widget { + min-height: 24px; + } +} diff --git a/frontend/components/YamlAce/index.js b/frontend/components/YamlAce/index.js new file mode 100644 index 000000000..0b49dc55c --- /dev/null +++ b/frontend/components/YamlAce/index.js @@ -0,0 +1 @@ +export default from './YamlAce'; diff --git a/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx index 6d84bc20c..7adc81493 100644 --- a/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx +++ b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import FileSaver from 'file-saver'; import Button from 'components/buttons/Button'; import enrollSecretInterface from 'interfaces/enroll_secret'; @@ -35,6 +36,20 @@ class EnrollSecretRow extends Component { return false; } + onDownloadSecret = (evt) => { + evt.preventDefault(); + + const { secret } = this.props; + + const filename = 'secret.txt'; + const file = new global.window.File([secret], filename); + + FileSaver.saveAs(file); + + return false; + } + + onToggleSecret = (evt) => { evt.preventDefault(); @@ -47,7 +62,7 @@ class EnrollSecretRow extends Component { renderLabel = () => { const { name } = this.props; const { showSecret, copyMessage } = this.state; - const { onCopySecret, onToggleSecret } = this; + const { onCopySecret, onDownloadSecret, onToggleSecret } = this; return ( @@ -61,6 +76,16 @@ class EnrollSecretRow extends Component { > + | + + Download + + | { + const errors = {}; + const { + error: yamlError, + valid: yamlValid, + } = validateYaml(formData.osquery_options); + + if (!yamlValid) { + errors.osquery_options = constructErrorString(yamlError); + } + + const valid = !size(errors); + return { valid, errors }; +}; + +class OsqueryOptionsForm extends Component { + static propTypes = { + formData: PropTypes.object, // eslint-disable-line react/forbid-prop-types + handleSubmit: PropTypes.func.isRequired, + fields: PropTypes.shape({ + osquery_options: formFieldInterface.isRequired, + }).isRequired, + } + + render () { + const { handleSubmit, fields } = this.props; + + return ( +
+

The YAML code editor allows control over osquery configuration options. + Options specified in the code editor below will overwrite existing osquery options. +

+ + + + ); + } +} + +export default Form(OsqueryOptionsForm, { + fields: ['osquery_options'], + validate, +}); diff --git a/frontend/components/forms/admin/OsqueryOptionsForm/_styles.scss b/frontend/components/forms/admin/OsqueryOptionsForm/_styles.scss new file mode 100644 index 000000000..3797522db --- /dev/null +++ b/frontend/components/forms/admin/OsqueryOptionsForm/_styles.scss @@ -0,0 +1,32 @@ +.osquery-options-form { + display: flex; + flex-direction: column; + align-items: flex-end; + width: 60%; + padding-right: 40px; + + &__header { + font-size: 16px; + margin: 15px 0; + display: inline-block; + color: $text-dark; + } + + &__button-wrap { + text-align: right; + + .query-form__run-query-btn, + .query-form__stop-query-btn { + margin-left: $pad-xsmall; + } + + .kolide-timer { + display: block; + } + } + + &__text-editor-wrapper { + margin: $base 0; + width: 100%; + } +} diff --git a/frontend/components/forms/admin/OsqueryOptionsForm/helpers.js b/frontend/components/forms/admin/OsqueryOptionsForm/helpers.js new file mode 100644 index 000000000..a4cd3c020 --- /dev/null +++ b/frontend/components/forms/admin/OsqueryOptionsForm/helpers.js @@ -0,0 +1,5 @@ +const constructErrorString = (yamlError) => { + return `${yamlError.name}: ${yamlError.reason} at line ${yamlError.line}`; +}; + +export default constructErrorString; diff --git a/frontend/components/forms/admin/OsqueryOptionsForm/index.js b/frontend/components/forms/admin/OsqueryOptionsForm/index.js new file mode 100644 index 000000000..d4c9d117f --- /dev/null +++ b/frontend/components/forms/admin/OsqueryOptionsForm/index.js @@ -0,0 +1 @@ +export default from './OsqueryOptionsForm'; diff --git a/frontend/components/forms/validators/validate_yaml/index.js b/frontend/components/forms/validators/validate_yaml/index.js new file mode 100644 index 000000000..22df60a38 --- /dev/null +++ b/frontend/components/forms/validators/validate_yaml/index.js @@ -0,0 +1,31 @@ +const yaml = require('js-yaml'); + +const invalidYamlResponse = (message) => { + return { valid: false, error: message }; +}; + +const validYamlResponse = { valid: true, error: null }; + +export const validateYaml = (yamlText) => { + if (!yamlText) { + return invalidYamlResponse('YAML text must be present'); + } + + try { + yaml.safeLoad(yamlText); + + return validYamlResponse; + } catch (error) { + if (error instanceof yaml.YAMLException) { + return invalidYamlResponse({ + name: 'Syntax Error', + reason: error.reason, + line: error.mark.line, + }); + } + + return invalidYamlResponse(error.message); + } +}; + +export default validateYaml; diff --git a/frontend/components/forms/validators/validate_yaml/validate_yaml.tests.js b/frontend/components/forms/validators/validate_yaml/validate_yaml.tests.js new file mode 100644 index 000000000..dd0380438 --- /dev/null +++ b/frontend/components/forms/validators/validate_yaml/validate_yaml.tests.js @@ -0,0 +1,42 @@ +import expect from 'expect'; + +import validateYaml from './index'; + +// Valid indentations take up two spaces +const malformedYaml = [ + 'spec:\nconfig:\n options:\n logger_plugin: tls\n pack_delimiter: /\n logger_tls_period: 10\n distributed_plugin: tls\n disable_distributed: false\n logger_tls_endpoint: /api/v1/osquery/log\n distributed_interval: 8\n distributed_tls_max_attempts: 5\n decorators:\n load:\n - SELECT uuid AS host_uuid FROM system_info;\n - SELECT hostname FROM system_info;\n overrides: {}\n', + + 'spec:\nconfig:\n options:\n logger_plugin: tls\n pack_delimiter /\n logger_tls_period: 10\n distributed_plugin: tls\n disable_distributed: false\n logger_tls_endpoint: /api/v1/osquery/log\n distributed_interval: 8\n distributed_tls_max_attempts: 5\n decorators:\n load:\n - SELECT uuid AS host_uuid FROM system_info;\n - SELECT hostname FROM system_info;\n overrides: {}\n', +]; + +const validYaml = [ + 'spec:\n config:\n options:\n logger_plugin: tls\n pack_delimiter: /\n logger_tls_period: 10\n distributed_plugin: tls\n disable_distributed: false\n logger_tls_endpoint: /api/v1/osquery/log\n distributed_interval: 8\n distributed_tls_max_attempts: 5\n decorators:\n load:\n - SELECT uuid AS host_uuid FROM system_info;\n - SELECT hostname FROM system_info;\n overrides: {}\n', +]; + +describe('validateYaml', () => { + it('rejects malformed yaml', () => { + malformedYaml.forEach((yaml) => { + const { error, valid } = validateYaml(yaml); + + expect(valid).toEqual(false); + expect(error.name).toEqual('Syntax Error'); + expect(error.reason).toExist(); + expect(error.line).toBeGreaterThan(0); + }); + }); + + it('rejects blank entries', () => { + const { error, valid } = validateYaml(); + + expect(valid).toEqual(false); + expect(error).toEqual('YAML text must be present'); + }); + + it('accepts valid yaml', () => { + validYaml.forEach((yaml) => { + const { error, valid } = validateYaml(yaml); + expect(valid).toEqual(true); + expect(error).toNotExist(); + }); + }); +}); diff --git a/frontend/components/hosts/AddHostModal/AddHostModal.jsx b/frontend/components/hosts/AddHostModal/AddHostModal.jsx index 69438e459..7434649ba 100644 --- a/frontend/components/hosts/AddHostModal/AddHostModal.jsx +++ b/frontend/components/hosts/AddHostModal/AddHostModal.jsx @@ -1,69 +1,158 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import FileSaver from 'file-saver'; +import Kolide from 'kolide'; import Button from 'components/buttons/Button'; +import configInterface from 'interfaces/config'; import enrollSecretInterface from 'interfaces/enroll_secret'; import EnrollSecretTable from 'components/config/EnrollSecretTable'; import Icon from 'components/icons/Icon'; -import certificate from '../../../../assets/images/osquery-certificate.svg'; const baseClass = 'add-host-modal'; + class AddHostModal extends Component { static propTypes = { - onFetchCertificate: PropTypes.func, onReturnToApp: PropTypes.func, enrollSecret: enrollSecretInterface, + config: configInterface, }; + constructor(props) { + super(props); + this.state = { fetchCertificateError: undefined }; + } + + componentDidMount() { + Kolide.config.loadCertificate() + .then((certificate) => { + this.setState({ certificate }); + }) + .catch(() => { + this.setState({ fetchCertificateError: 'Failed to load certificate. Is Fleet App URL configured properly?' }); + }); + } + + onFetchCertificate = (evt) => { + evt.preventDefault(); + + const { certificate } = this.state; + + const filename = 'fleet.pem'; + const file = new global.window.File([certificate], filename, { type: 'application/x-pem-file' }); + + FileSaver.saveAs(file); + + return false; + } + render() { const { - onFetchCertificate, + config, onReturnToApp, enrollSecret, } = this.props; + const { fetchCertificateError } = this.state; + + let tlsHostname = config.kolide_server_url; + try { + const serverUrl = new URL(config.kolide_server_url); + tlsHostname = serverUrl.hostname; + if (serverUrl.port) { + tlsHostname += `:${serverUrl.port}`; + } + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + + const flagfileContent = `--enroll_secret_path=secret.txt +--tls_server_certs=fleet.pem +--tls_hostname=${tlsHostname} +--host_identifier=uuid +--enroll_tls_endpoint=/api/v1/osquery/enroll +--config_plugin=tls +--config_tls_endpoint=/api/v1/osquery/config +--config_refresh=10 +--disable_distributed=false +--distributed_plugin=tls +--distributed_interval=10 +--distributed_tls_max_attempts=3 +--distributed_tls_read_endpoint=/api/v1/osquery/distributed/read +--distributed_tls_write_endpoint=/api/v1/osquery/distributed/write +--logger_plugin=tls +--logger_tls_endpoint=/api/v1/osquery/log +--logger_tls_period=10 +--disable_carver=false +--carver_start_endpoint=/api/v1/osquery/carve/begin +--carver_continue_endpoint=/api/v1/osquery/carve/block +--carver_block_size=2000000`; + + const onDownloadFlagfile = (evt) => { + evt.preventDefault(); + + const filename = 'flagfile.txt'; + const file = new global.window.File([flagfileContent], filename); + + FileSaver.saveAs(file); + + return false; + }; + return (
-

- Follow the instructions below to add hosts to your Fleet Instance. -

-
+
  1. -

    - - Fleet / Osquery - Install Docs - -

    -
  2. -
  3. -

    Osquery Enroll Secret

    +

    1. Enroll Secret

    - Provide osquery with one of the following active enroll secrets: + Provide an active enroll secret to allow osquery to authenticate with the Fleet server:

  4. -

    Download Server Certificate (Optional)

    +

    2. Server Certificate

    +

    + Provide the TLS certificate used by the Fleet server to enable secure connections from osquery: +

    +

    + { fetchCertificateError + ? {fetchCertificateError} + : Download Certificate + } +

    +
  5. +
  6. +

    3. Flagfile

    +

    + If using the enroll secret and server certificate downloaded above, use the generated flagfile. In some configurations, modifications may need to be made: +

    - If you use the native osquery TLS plugins, Osquery requires the - same TLS certificate that Fleet is using in order to - authenticate. You can fetch the certificate below: + Download Flagfile

    -

    - +

  7. +
  8. +

    4. Run Osquery

    +

    + Run osquery from the directory containing the above files (may require sudo or Run as Administrator privileges):

    +
    osqueryd --flagfile=flagfile.txt --verbose
diff --git a/frontend/components/hosts/AddHostModal/_styles.scss b/frontend/components/hosts/AddHostModal/_styles.scss index 96c6f7ade..b17f009bd 100644 --- a/frontend/components/hosts/AddHostModal/_styles.scss +++ b/frontend/components/hosts/AddHostModal/_styles.scss @@ -46,14 +46,14 @@ margin-left: 10px; padding: 20px; box-sizing: border-box; - + h4 { font-size: 16px; font-weight: $bold; line-height: 1.5; letter-spacing: -0.5px; color: rgba(32, 37, 50, 0.66); - margin: 40px 0 0; + margin: 10px 0 0; .kolidecon { margin-left: 5px; @@ -75,6 +75,12 @@ } } + &__documentation-link { + h4 { + margin: 0; + } + } + &__install-steps { margin: 0; padding: 0; @@ -134,4 +140,14 @@ color: $link; } } + + pre, code { + background-color: #f9f9f9; + } + + &__error { + color: $alert; + } + } + diff --git a/frontend/components/hosts/HostContainer/HostContainer.jsx b/frontend/components/hosts/HostContainer/HostContainer.jsx index d3741d16c..04d5a515d 100644 --- a/frontend/components/hosts/HostContainer/HostContainer.jsx +++ b/frontend/components/hosts/HostContainer/HostContainer.jsx @@ -38,7 +38,7 @@ class HostContainer extends Component {

Still having trouble?

-

File a Github issue.

+

File a Github issue.

); diff --git a/frontend/components/side_panels/SiteNavSidePanel/navItems.js b/frontend/components/side_panels/SiteNavSidePanel/navItems.js index 9a45f48d0..edcf4cfea 100644 --- a/frontend/components/side_panels/SiteNavSidePanel/navItems.js +++ b/frontend/components/side_panels/SiteNavSidePanel/navItems.js @@ -27,6 +27,16 @@ export default (admin) => { pathname: PATHS.ADMIN_SETTINGS, }, }, + { + // No such icon now. SiteNavSidePanel does not display + // icons for subItems + icon: 'osquery', + name: 'Osquery Options', + location: { + regex: new RegExp(`^${PATHS.ADMIN_OSQUERY}`), + pathname: PATHS.ADMIN_OSQUERY, + }, + }, ], }, ]; @@ -98,7 +108,7 @@ export default (admin) => { name: 'Help', location: { regex: /^\/help/, - pathname: 'https://github.com/kolide/fleet/blob/master/docs/README.md', + pathname: 'https://github.com/fleetdm/fleet/blob/master/docs/README.md', }, subItems: [], }, diff --git a/frontend/interfaces/errors500.js b/frontend/interfaces/errors500.js new file mode 100644 index 000000000..39b02123c --- /dev/null +++ b/frontend/interfaces/errors500.js @@ -0,0 +1,6 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.shape({ + http_status: PropTypes.number, + base: PropTypes.string, +}); diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js index 69ab871f6..8eff0a820 100644 --- a/frontend/kolide/endpoints.js +++ b/frontend/kolide/endpoints.js @@ -4,6 +4,7 @@ export default { CONFIRM_EMAIL_CHANGE: (token) => { return `/v1/kolide/email/change/${token}`; }, + OSQUERY_OPTIONS: '/v1/kolide/spec/osquery_options', ENABLE_USER: (id) => { return `/v1/kolide/users/${id}/enable`; }, diff --git a/frontend/kolide/entities/osquery_options.js b/frontend/kolide/entities/osquery_options.js new file mode 100644 index 000000000..f0aba691a --- /dev/null +++ b/frontend/kolide/entities/osquery_options.js @@ -0,0 +1,18 @@ +import endpoints from 'kolide/endpoints'; + +const yaml = require('js-yaml'); + +export default (client) => { + return { + loadAll: () => { + const { OSQUERY_OPTIONS } = endpoints; + return client.authenticatedGet(client._endpoint(OSQUERY_OPTIONS)); + }, + update: (formData) => { + const { OSQUERY_OPTIONS } = endpoints; + const osqueryOptionsData = yaml.safeLoad(formData.osquery_options); + + return client.authenticatedPost(client._endpoint(OSQUERY_OPTIONS), JSON.stringify(osqueryOptionsData)); + }, + }; +}; diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index febbcbde1..4a39ef633 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -2,6 +2,7 @@ import Base from 'kolide/base'; import Request from 'kolide/request'; import accountMethods from 'kolide/entities/account'; import configMethods from 'kolide/entities/config'; +import osqueryOptionsMethods from 'kolide/entities/osquery_options'; import hostMethods from 'kolide/entities/hosts'; import inviteMethods from 'kolide/entities/invites'; import labelMethods from 'kolide/entities/labels'; @@ -23,6 +24,7 @@ class Kolide extends Base { this.account = accountMethods(this); this.config = configMethods(this); + this.osqueryOptions = osqueryOptionsMethods(this); this.hosts = hostMethods(this); this.invites = inviteMethods(this); this.labels = labelMethods(this); diff --git a/frontend/pages/Kolide404/Kolide404.jsx b/frontend/pages/Kolide404/Kolide404.jsx index 1b317b987..34bacc864 100644 --- a/frontend/pages/Kolide404/Kolide404.jsx +++ b/frontend/pages/Kolide404/Kolide404.jsx @@ -21,7 +21,7 @@ class Kolide404 extends Component {

Might we recommend going back on your browser or visiting the home page?

-

Need assistance? File an issue.

+

Need assistance? File an issue.

diff --git a/frontend/pages/Kolide500/Kolide500.jsx b/frontend/pages/Kolide500/Kolide500.jsx index dd9282f41..a0772c432 100644 --- a/frontend/pages/Kolide500/Kolide500.jsx +++ b/frontend/pages/Kolide500/Kolide500.jsx @@ -1,12 +1,72 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { noop } from 'lodash'; +import { resetErrors } from 'redux/nodes/errors500/actions'; +import errorsInterface from 'interfaces/errors500'; import kolideLogo from '../../../assets/images/kolide-logo-condensed.svg'; import gopher from '../../../assets/images/500.svg'; const baseClass = 'kolide-500'; -class Kolide404 extends Component { +class Kolide500 extends Component { + static propTypes = { + errors: errorsInterface, + dispatch: PropTypes.func, + }; + + static defaultProps = { + dispatch: noop, + }; + + constructor (props) { + super(props); + + this.state = { + showErrorMessage: false, + }; + } + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch(resetErrors()); + } + + onShowErrorMessage = () => { + this.setState({ showErrorMessage: true }); + } + + renderError = () => { + const { errors } = this.props; + const errorMessage = errors ? errors.base : null; + const { showErrorMessage } = this.state; + const { onShowErrorMessage } = this; + + if (errorMessage && !showErrorMessage) { + // We only show the button when errorMessage exists + // and showErrorMessage is set to false + return ( + + ); + } + + if (errorMessage && showErrorMessage) { + // We only show the error message when errorMessage exists + // and showErrorMessage is set to true + return ( +
+

{errorMessage}

+
+ ); + } + + return false; + } + render () { + const { renderError } = this; + return (
@@ -18,10 +78,17 @@ class Kolide404 extends Component {

Uh oh!

Error 500

Something went wrong on our end.

-

We have alerted the engineers and they are working on a solution.

+ {renderError()} +

Please file an issue if you believe this is a bug.

+ + File an issue +
-

Need assistance? File an issue.

@@ -29,4 +96,11 @@ class Kolide404 extends Component { } } -export default Kolide404; +const mapStateToProps = (state) => { + const { errors } = state.errors500; + return { + errors, + }; +}; + +export default connect(mapStateToProps)(Kolide500); diff --git a/frontend/pages/Kolide500/_styles.scss b/frontend/pages/Kolide500/_styles.scss index 62f835200..775818d2c 100644 --- a/frontend/pages/Kolide500/_styles.scss +++ b/frontend/pages/Kolide500/_styles.scss @@ -36,6 +36,10 @@ } } + .error-message-container { + display: inline; + } + main { text-align: center; diff --git a/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.jsx b/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.jsx new file mode 100644 index 000000000..dfa3b536a --- /dev/null +++ b/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.jsx @@ -0,0 +1,111 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { noop } from 'lodash'; + +import osqueryOptionsActions from 'redux/nodes/osquery/actions'; +import validateYaml from 'components/forms/validators/validate_yaml'; +import OsqueryOptionsForm from 'components/forms/admin/OsqueryOptionsForm'; +import Icon from 'components/icons/Icon'; +import { renderFlash } from 'redux/nodes/notifications/actions'; + +const yaml = require('js-yaml'); + +const baseClass = 'osquery-options'; + +export class OsqueryOptionsPage extends Component { + static propTypes = { + options: PropTypes.object, // eslint-disable-line react/forbid-prop-types + dispatch: PropTypes.func, + }; + + static defaultProps = { + dispatch: noop, + } + + componentDidMount() { + const { dispatch } = this.props; + dispatch(osqueryOptionsActions.getOsqueryOptions()) + .catch(() => false); + } + + onSaveOsqueryOptionsFormSubmit = (formData) => { + const { dispatch } = this.props; + const { error } = validateYaml(formData.osquery_options); + + if (error) { + dispatch(renderFlash('error', error)); + + return false; + } + + dispatch(osqueryOptionsActions.updateOsqueryOptions(formData)) + .then(() => { + dispatch(renderFlash('success', 'Osquery options updated!')); + + return false; + }) + .catch((errors) => { + if (errors.base) { + dispatch(renderFlash('error', errors.base)); + } + + return false; + }); + + return false; + } + + render () { + const { options } = this.props; + const formData = { + osquery_options: yaml.safeDump(options), + }; + const { onSaveOsqueryOptionsFormSubmit } = this; + + return ( +
+

Osquery Options

+
+ +
+

This file describes options returned to osqueryd when it checks for configuration.

+

See Fleet documentation for an example file that includes the overrides option.

+ + GO TO FLEET DOCS + + +

See osquery documentation for all available options.

+ + GO TO OSQUERY DOCS + + +
+
+
+ ); + } +} + +const mapStateToProps = (state) => { + const { osquery } = state; + const { options } = osquery; + return { + options, + }; +}; + +export default connect(mapStateToProps)(OsqueryOptionsPage); diff --git a/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.tests.jsx b/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.tests.jsx new file mode 100644 index 000000000..c3f09caf3 --- /dev/null +++ b/frontend/pages/admin/OsqueryOptionsPage/OsqueryOptionsPage.tests.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import expect, { restoreSpies, spyOn } from 'expect'; +import { mount } from 'enzyme'; + +import { connectedComponent, reduxMockStore } from 'test/helpers'; +import ConnectedOsqueryOptionsPage, { OsqueryOptionsPage } from 'pages/admin/OsqueryOptionsPage/OsqueryOptionsPage'; +import osqueryOptionsActions from 'redux/nodes/osquery/actions'; + +const currentUser = { + admin: true, + email: 'hi@gnar.dog', + enabled: true, + name: 'Gnar Dog', + position: 'Head of Gnar', + username: 'gnardog', +}; + +const osqueryOptionsString = + 'spec:\n config:\n options:\n logger_plugin: tls\n pack_delimiter: /\n logger_tls_period: 10\n distributed_plugin: tls\n disable_distributed: false\n logger_tls_endpoint: /api/v1/osquery/log\n distributed_interval: 8\n distributed_tls_max_attempts: 5\n decorators:\n load:\n - SELECT uuid AS host_uuid FROM system_info;\n - SELECT hostname FROM system_info;\n overrides: {}\n'; + +const store = { + app: { + config: { + configured: true, + }, + }, + auth: { + user: { + ...currentUser, + }, + }, + osquery: { + erros: {}, + loading: false, + options: {}, + }, + entities: { + users: { + loading: false, + data: { + 1: { + ...currentUser, + }, + }, + }, + }, +}; + +describe('Osquery Options Page - Component', () => { + beforeEach(() => { + spyOn(osqueryOptionsActions, 'getOsqueryOptions') + .andReturn(() => Promise.resolve([])); + + spyOn(osqueryOptionsActions, 'updateOsqueryOptions') + .andReturn(() => Promise.resolve([])); + }); + + afterEach(restoreSpies); + + it('renders', () => { + const mockStore = reduxMockStore(store); + const page = mount(connectedComponent(ConnectedOsqueryOptionsPage, { mockStore })); + + expect(page.find('OsqueryOptionsPage').length).toEqual(1); + }); + + it('gets osquery options on mount', () => { + const mockStore = reduxMockStore(store); + + mount(connectedComponent(ConnectedOsqueryOptionsPage, { mockStore })); + + expect(osqueryOptionsActions.getOsqueryOptions).toHaveBeenCalled(); + }); + + describe('updating osquery options', () => { + const dispatch = () => Promise.resolve(); + const props = { dispatch, options: {} }; + const pageNode = mount().instance(); + const updatedOptions = { osquery_options: osqueryOptionsString }; + + it('updates the current osquery options with the new osquery options object', () => { + spyOn(osqueryOptionsActions, 'updateOsqueryOptions').andCallThrough(); + + pageNode.onSaveOsqueryOptionsFormSubmit(updatedOptions); + + expect(osqueryOptionsActions.updateOsqueryOptions).toHaveBeenCalledWith(updatedOptions); + }); + }); +}); diff --git a/frontend/pages/admin/OsqueryOptionsPage/_styles.scss b/frontend/pages/admin/OsqueryOptionsPage/_styles.scss new file mode 100644 index 000000000..f5e0b0a3a --- /dev/null +++ b/frontend/pages/admin/OsqueryOptionsPage/_styles.scss @@ -0,0 +1,32 @@ +.osquery-options { + padding: 30px; + + h1 { + margin: 0 0 22px; + } + + a { + margin: 0 0 20px; + } + + &__form-wrapper { + display: flex; + } + + &__form-details { + margin-top: 133px; + width: 40%; + + p { + font-size: 15px; + font-weight: $normal; + line-height: 1.6; + letter-spacing: 0.5px; + color: $text-dark; + } + } + + i { + margin-left: 8px; + } +} diff --git a/frontend/pages/admin/OsqueryOptionsPage/index.js b/frontend/pages/admin/OsqueryOptionsPage/index.js new file mode 100644 index 000000000..4287e184c --- /dev/null +++ b/frontend/pages/admin/OsqueryOptionsPage/index.js @@ -0,0 +1 @@ +export default from './OsqueryOptionsPage'; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index f9a8f6cea..8aa2ba9dc 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -2,14 +2,13 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import AceEditor from 'react-ace'; import { connect } from 'react-redux'; -import FileSaver from 'file-saver'; import { push } from 'react-router-redux'; import { sortBy } from 'lodash'; import classNames from 'classnames'; -import Kolide from 'kolide'; import AddHostModal from 'components/hosts/AddHostModal'; import Button from 'components/buttons/Button'; +import configInterface from 'interfaces/config'; import HostContainer from 'components/hosts/HostContainer'; import HostPagination from 'components/hosts/HostPagination'; import HostSidePanel from 'components/side_panels/HostSidePanel'; @@ -45,6 +44,7 @@ const baseClass = 'manage-hosts'; export class ManageHostsPage extends PureComponent { static propTypes = { + config: configInterface, dispatch: PropTypes.func, display: PropTypes.oneOf(['Grid', 'List']), hosts: PropTypes.arrayOf(hostInterface), @@ -155,18 +155,6 @@ export class ManageHostsPage extends PureComponent { .catch(() => false); } - onFetchCertificate = () => { - return Kolide.config.loadCertificate() - .then((certificate) => { - const filename = `${global.window.location.host}.pem`; - const file = new global.window.File([certificate], filename, { type: 'application/x-pem-file' }); - - FileSaver.saveAs(file); - - return false; - }); - } - onLabelClick = (selectedLabel) => { return (evt) => { evt.preventDefault(); @@ -304,9 +292,9 @@ export class ManageHostsPage extends PureComponent { } renderAddHostModal = () => { - const { onFetchCertificate, toggleAddHostModal } = this; + const { toggleAddHostModal } = this; const { showAddHostModal } = this.state; - const { enrollSecret } = this.props; + const { enrollSecret, config } = this.props; if (!showAddHostModal) { return false; @@ -319,9 +307,9 @@ export class ManageHostsPage extends PureComponent { className={`${baseClass}__invite-modal`} > ); @@ -668,6 +656,7 @@ const mapStateToProps = (state, { location, params }) => { const { errors: labelErrors, loading: loadingLabels } = state.entities.labels; const { loading: loadingHosts } = state.entities.hosts; const enrollSecret = state.app.enrollSecret; + const config = state.app.config; return { selectedFilter, @@ -684,6 +673,7 @@ const mapStateToProps = (state, { location, params }) => { selectedLabel, selectedOsqueryTable, statusLabels, + config, }; }; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index 23322ee07..c16e19ca9 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -7,7 +7,7 @@ import hostActions from 'redux/nodes/entities/hosts/actions'; import labelActions from 'redux/nodes/entities/labels/actions'; import ConnectedManageHostsPage, { ManageHostsPage } from 'pages/hosts/ManageHostsPage/ManageHostsPage'; import { connectedComponent, createAceSpy, reduxMockStore, stubbedOsqueryTable } from 'test/helpers'; -import { hostStub } from 'test/stubs'; +import { hostStub, configStub } from 'test/stubs'; import * as manageHostsPageActions from 'redux/nodes/components/ManageHostsPage/actions'; const allHostsLabel = { id: 1, display_text: 'All Hosts', slug: 'all-hosts', type: 'all', count: 22 }; @@ -49,6 +49,7 @@ const mockStore = reduxMockStore({ describe('ManageHostsPage - component', () => { const props = { + config: configStub, dispatch: noop, hosts: [], labels: [], diff --git a/frontend/redux/nodes/errors500/actions.js b/frontend/redux/nodes/errors500/actions.js new file mode 100644 index 000000000..2b4c2e4f3 --- /dev/null +++ b/frontend/redux/nodes/errors500/actions.js @@ -0,0 +1,7 @@ +export const RESET_ERRORS = 'RESET_ERRORS'; + +export const resetErrors = () => { + return { + type: RESET_ERRORS, + }; +}; diff --git a/frontend/redux/nodes/errors500/reducer.js b/frontend/redux/nodes/errors500/reducer.js new file mode 100644 index 000000000..03fe16f08 --- /dev/null +++ b/frontend/redux/nodes/errors500/reducer.js @@ -0,0 +1,22 @@ +import { + RESET_ERRORS, +} from './actions'; + +const initialState = { + errors: null, +}; + +const reducer = (state = initialState, { type, payload }) => { + if (payload && payload.errors) { + return { + errors: payload.errors, + }; + } else if (type === RESET_ERRORS) { + return { + errors: null, + }; + } + return state; +}; + +export default reducer; diff --git a/frontend/redux/nodes/errors500/reducer.tests.js b/frontend/redux/nodes/errors500/reducer.tests.js new file mode 100644 index 000000000..6adada622 --- /dev/null +++ b/frontend/redux/nodes/errors500/reducer.tests.js @@ -0,0 +1,39 @@ +import expect from 'expect'; + +import reducer from './reducer'; + +describe('Errors - reducer', () => { + it('Updates state with errors object when an action that has a payload with an errors object is dispatched', () => { + const payload = { + errors: { + base: "inserting pack: Error 1136: Column count doesn't match value count at row 1", + http_status: 500, + }, + }; + const packsCreateFailureAction = { type: 'packs_CREATE_FAILURE', payload }; + const initialState = { + errors: null, + }; + const newState = reducer(initialState, packsCreateFailureAction); + + expect(newState).toEqual({ + errors: { + base: "inserting pack: Error 1136: Column count doesn't match value count at row 1", + http_status: 500, + }, + }); + }); + + it('Updates state by setting errors to null when the RESET_ERRORS action is dipatched', () => { + const errorsState = { + errors: { + base: "inserting pack: Error 1136: Column count doesn't match value count at row 1", + http_status: 500, + }, + }; + const newState = reducer(errorsState, { type: 'RESET_ERRORS' }); + expect(newState).toEqual({ + errors: null, + }); + }); +}); diff --git a/frontend/redux/nodes/osquery/actions.js b/frontend/redux/nodes/osquery/actions.js new file mode 100644 index 000000000..0a0293548 --- /dev/null +++ b/frontend/redux/nodes/osquery/actions.js @@ -0,0 +1,58 @@ +import Kolide from 'kolide'; + +const yaml = require('js-yaml'); + +export const OSQUERY_OPTIONS_FAILURE = 'OSQUERY_OPTIONS_FAILURE'; +export const OSQUERY_OPTIONS_START = 'OSQUERY_OPTIONS_START'; +export const OSQUERY_OPTIONS_SUCCESS = 'OSQUERY_OPTIONS_SUCCESS'; + +export const loadOsqueryOptions = { type: OSQUERY_OPTIONS_START }; + +export const osqueryOptionsSuccess = (data) => { + return { type: OSQUERY_OPTIONS_SUCCESS, payload: { data } }; +}; + +export const osqueryOptionsFailure = (errors) => { + return { type: OSQUERY_OPTIONS_FAILURE, payload: { errors } }; +}; + +export const getOsqueryOptions = () => { + return (dispatch) => { + dispatch(loadOsqueryOptions); + + return Kolide.osqueryOptions.loadAll() + .then((osqueryOptions) => { + dispatch(osqueryOptionsSuccess(osqueryOptions)); + + return osqueryOptions; + }) + .catch((errors) => { + dispatch(osqueryOptionsFailure(errors)); + + throw errors; + }); + }; +}; + +export const updateOsqueryOptions = (osqueryOptionsData) => { + return (dispatch) => { + dispatch(loadOsqueryOptions); + return Kolide.osqueryOptions.update(osqueryOptionsData) + .then((osqueryOptions) => { + const yamlOptions = yaml.safeLoad(osqueryOptionsData.osquery_options); + dispatch(osqueryOptionsSuccess(yamlOptions)); + + return osqueryOptions; + }) + .catch((errors) => { + dispatch(osqueryOptionsFailure(errors)); + + throw errors; + }); + }; +}; + +export default { + getOsqueryOptions, + updateOsqueryOptions, +}; diff --git a/frontend/redux/nodes/osquery/reducer.js b/frontend/redux/nodes/osquery/reducer.js new file mode 100644 index 000000000..e5ac04b1d --- /dev/null +++ b/frontend/redux/nodes/osquery/reducer.js @@ -0,0 +1,37 @@ +import { + OSQUERY_OPTIONS_FAILURE, + OSQUERY_OPTIONS_START, + OSQUERY_OPTIONS_SUCCESS, +} from './actions'; + +export const initialState = { + options: {}, + errors: {}, + loading: false, +}; + +const reducer = (state = initialState, { type, payload }) => { + switch (type) { + case OSQUERY_OPTIONS_START: + return { + ...state, + loading: true, + }; + case OSQUERY_OPTIONS_SUCCESS: + return { + ...state, + options: payload.data, + loading: false, + }; + case OSQUERY_OPTIONS_FAILURE: + return { + ...state, + errors: payload.errors, + loading: false, + }; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/redux/nodes/osquery/reducer.tests.js b/frontend/redux/nodes/osquery/reducer.tests.js new file mode 100644 index 000000000..bcf30c557 --- /dev/null +++ b/frontend/redux/nodes/osquery/reducer.tests.js @@ -0,0 +1,47 @@ +import expect from 'expect'; + +import reducer, { initialState } from './reducer'; +import { + loadOsqueryOptions, + osqueryOptionsFailure, + osqueryOptionsSuccess, +} from './actions'; + +describe('Osquery - reducer', () => { + it('sets the initial state', () => { + expect(reducer(undefined, { type: 'SOME_ACTION' })).toEqual(initialState); + }); + + it('sets the state to loading', () => { + expect(reducer(initialState, loadOsqueryOptions)).toEqual({ + ...initialState, + loading: true, + }); + }); + + it('sets the osquery options in state', () => { + const osqueryOptions = { spec: {} }; + const loadingOsqueryOptionsState = { + ...initialState, + loading: true, + }; + expect(reducer(loadingOsqueryOptionsState, osqueryOptionsSuccess(osqueryOptions))).toEqual({ + loading: false, + errors: {}, + options: osqueryOptions, + }); + }); + + it('sets errors in state', () => { + const error = 'Unable to get osquery options'; + const loadingOsqueryOptionsState = { + ...initialState, + loading: true, + }; + expect(reducer(loadingOsqueryOptionsState, osqueryOptionsFailure(error))).toEqual({ + loading: false, + errors: error, + options: {}, + }); + }); +}); diff --git a/frontend/redux/reducers.js b/frontend/redux/reducers.js index 87426a618..cc29eeb12 100644 --- a/frontend/redux/reducers.js +++ b/frontend/redux/reducers.js @@ -6,7 +6,9 @@ import app from './nodes/app/reducer'; import auth from './nodes/auth/reducer'; import components from './nodes/components/reducer'; import entities from './nodes/entities/reducer'; +import errors500 from './nodes/errors500/reducer'; import notifications from './nodes/notifications/reducer'; +import osquery from './nodes/osquery/reducer'; import persistentFlash from './nodes/persistent_flash/reducer'; import redirectLocation from './nodes/redirectLocation/reducer'; @@ -15,8 +17,10 @@ export default combineReducers({ auth, components, entities, + errors500, loadingBar: loadingBarReducer, notifications, + osquery, persistentFlash, redirectLocation, routing: routerReducer, diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx index 650122934..7f085fad0 100644 --- a/frontend/router/index.jsx +++ b/frontend/router/index.jsx @@ -5,6 +5,7 @@ import { syncHistoryWithStore } from 'react-router-redux'; import AdminAppSettingsPage from 'pages/admin/AppSettingsPage'; import AdminUserManagementPage from 'pages/admin/UserManagementPage'; +import AdminOsqueryOptionsPage from 'pages/admin/OsqueryOptionsPage'; import AllPacksPage from 'pages/packs/AllPacksPage'; import App from 'components/App'; import AuthenticatedAdminRoutes from 'components/AuthenticatedAdminRoutes'; @@ -50,6 +51,7 @@ const routes = ( + diff --git a/frontend/router/paths.js b/frontend/router/paths.js index a45cf07c8..5bb6c7d38 100644 --- a/frontend/router/paths.js +++ b/frontend/router/paths.js @@ -3,6 +3,7 @@ import URL_PREFIX from 'router/url_prefix'; export default { ADMIN_USERS: `${URL_PREFIX}/admin/users`, ADMIN_SETTINGS: `${URL_PREFIX}/admin/settings`, + ADMIN_OSQUERY: `${URL_PREFIX}/admin/osquery`, ALL_PACKS: `${URL_PREFIX}/packs/all`, EDIT_PACK: (pack) => { return `${URL_PREFIX}/packs/${pack.id}/edit`; diff --git a/frontend/templates/react.ejs b/frontend/templates/react.ejs index 72ec664d0..a080e80a7 100644 --- a/frontend/templates/react.ejs +++ b/frontend/templates/react.ejs @@ -5,62 +5,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Kolide Fleet + Fleet for osquery diff --git a/go.mod b/go.mod index b62ffb622..e299edb88 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,11 @@ require ( github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea github.com/aws/aws-sdk-go v1.26.8 github.com/beevik/etree v1.1.0 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 github.com/cenkalti/backoff/v4 v4.0.0 + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 github.com/elazarl/go-bindata-assetfs v1.0.0 @@ -20,9 +23,10 @@ require ( github.com/go-kit/kit v0.8.0 github.com/go-sql-driver/mysql v1.5.0 github.com/gomodule/redigo v2.0.0+incompatible - github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect + github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c github.com/gorilla/mux v1.6.2 github.com/gorilla/websocket v1.4.2 + github.com/gosuri/uilive v0.0.4 github.com/hashicorp/golang-lru v0.5.1 // indirect github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect github.com/igm/sockjs-go v3.0.0+incompatible @@ -39,6 +43,7 @@ require ( github.com/mattn/go-runewidth v0.0.8 // indirect github.com/mattn/go-sqlite3 v1.11.0 // indirect github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 // indirect + github.com/mixer/clock v0.0.0-20200713181918-dd2ce6ac2af6 github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 github.com/pelletier/go-toml v1.1.0 // indirect @@ -53,11 +58,11 @@ require ( github.com/spf13/pflag v1.0.1 // indirect github.com/spf13/viper v1.0.2 github.com/stretchr/testify v1.5.1 - github.com/urfave/cli v1.20.0 + github.com/urfave/cli v1.22.4 go.opencensus.io v0.20.2 // indirect - golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a // indirect - golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 // indirect + golang.org/x/tools v0.0.0-20201102212025-f46e4245211d // indirect google.golang.org/api v0.3.2 // indirect google.golang.org/grpc v1.19.0 gopkg.in/guregu/null.v3 v3.4.0 diff --git a/go.sum b/go.sum index c1c9fe663..2233aa4c4 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 h1:osZyZB7J4kE1tKLeaUjV6+uZVBfS835T0I/RxmwWw1w= github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s= @@ -30,6 +32,10 @@ github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOC github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -81,6 +87,8 @@ github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= +github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -113,8 +121,10 @@ github.com/kolide/osquery-go v0.0.0-20190904034940-a74aa860032d h1:alVW+rIOMejar github.com/kolide/osquery-go v0.0.0-20190904034940-a74aa860032d/go.mod h1:QVQKz6+eKB0syu5u8BS1E4S/nTMuy44d9WbRFQ4nLnQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= @@ -135,6 +145,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mixer/clock v0.0.0-20200713181918-dd2ce6ac2af6 h1:8Ugm/CTuiRVYKox6tbwC0f9bY8a2pHIJ2oFg1r2LBS8= +github.com/mixer/clock v0.0.0-20200713181918-dd2ce6ac2af6/go.mod h1:8EnmexmqSZ7MbRW5Pg9H4bd0+lneHd6uAQya5Gpfq1k= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI= github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -170,6 +182,10 @@ github.com/russellhaering/gosaml2 v0.3.1 h1:s+Oz2RRS83uqocWhWdR8Gbtze4g84cWQqNUm github.com/russellhaering/gosaml2 v0.3.1/go.mod h1:niieRtQaw+opTVp9jzZo1nAAoksI2eNpd+weDcjZ+Mk= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -192,6 +208,9 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2 h1:NAfh7zF0/3/HqtMvJNZ/RFrSlCE6ZTlHmKfhL/Dm1Jk= @@ -200,10 +219,15 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -214,6 +238,9 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= @@ -222,8 +249,11 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180815093151-14742f9018cd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -238,14 +268,27 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201102212025-f46e4245211d h1:qbdJV2Z36oENmeAcKxD4qJx1FRdKoCzNewsdABS63dY= +golang.org/x/tools v0.0.0-20201102212025-f46e4245211d/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2 h1:iTp+3yyl/KOtxa/d1/JUE0GGSoR6FuW5udver22iwpw= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= diff --git a/package.json b/package.json index b299d9ba1..b92550bc4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "make test-js" }, "dependencies": { + "ace-builds": "1.3.1", "autoprefixer": "^9.4.3", "bourbon": "^5.0.0", "brace": "0.11.1", diff --git a/server/datastore/datastore_app_test.go b/server/datastore/datastore_app_test.go index 587e12c46..3ef66aaa9 100644 --- a/server/datastore/datastore_app_test.go +++ b/server/datastore/datastore_app_test.go @@ -118,7 +118,23 @@ func testEnrollSecrets(t *testing.T, ds kolide.Datastore) { name, err = ds.VerifyEnrollSecret("two_secret") assert.NoError(t, err) assert.Equal(t, "two", name) +} + +func testEnrollSecretsCaseSensitive(t *testing.T, ds kolide.Datastore) { + err := ds.ApplyEnrollSecretSpec( + &kolide.EnrollSecretSpec{ + Secrets: []kolide.EnrollSecret{ + kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: true}, + kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: false}, + }, + }, + ) + require.NoError(t, err) + _, err = ds.VerifyEnrollSecret("one_secret") + assert.NoError(t, err, "enroll secret should match with matching case") + _, err = ds.VerifyEnrollSecret("One_Secret") + assert.Error(t, err, "enroll secret with different case should not verify") } func testEnrollSecretRoundtrip(t *testing.T, ds kolide.Datastore) { diff --git a/server/datastore/datastore_carves_test.go b/server/datastore/datastore_carves_test.go new file mode 100644 index 000000000..ecd0d1ee4 --- /dev/null +++ b/server/datastore/datastore_carves_test.go @@ -0,0 +1,221 @@ +package datastore + +import ( + "crypto/rand" + "testing" + "time" + + "github.com/kolide/fleet/server/kolide" + "github.com/kolide/fleet/server/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testCarveMetadata(t *testing.T, ds kolide.Datastore) { + h := test.NewHost(t, ds, "foo.local", "192.168.1.10", "1", "1", time.Now()) + + expectedCarve := &kolide.CarveMetadata{ + HostId: h.ID, + Name: "foobar", + BlockCount: 10, + BlockSize: 12, + CarveSize: 123, + CarveId: "carve_id", + RequestId: "request_id", + SessionId: "session_id", + } + + expectedCarve, err := ds.NewCarve(expectedCarve) + require.NoError(t, err) + assert.NotEqual(t, 0, expectedCarve.ID) + expectedCarve.MaxBlock = -1 + + carve, err := ds.CarveBySessionId(expectedCarve.SessionId) + require.NoError(t, err) + expectedCarve.CreatedAt = carve.CreatedAt // Ignore created_at field + assert.Equal(t, expectedCarve, carve) + + carve, err = ds.Carve(expectedCarve.ID) + expectedCarve.CreatedAt = carve.CreatedAt // Ignore created_at field + require.NoError(t, err) + assert.Equal(t, expectedCarve, carve) + + // Check for increment of max block + + err = ds.NewBlock(carve.ID, 0, nil) + require.NoError(t, err) + expectedCarve.MaxBlock = 0 + + carve, err = ds.CarveBySessionId(expectedCarve.SessionId) + require.NoError(t, err) + assert.Equal(t, expectedCarve, carve) + + carve, err = ds.Carve(expectedCarve.ID) + require.NoError(t, err) + assert.Equal(t, expectedCarve, carve) + + // Check for increment of max block + + err = ds.NewBlock(carve.ID, 1, nil) + require.NoError(t, err) + expectedCarve.MaxBlock = 1 + + carve, err = ds.CarveBySessionId(expectedCarve.SessionId) + require.NoError(t, err) + assert.Equal(t, expectedCarve, carve) + + // Get by name also + carve, err = ds.CarveByName(expectedCarve.Name) + require.NoError(t, err) + assert.Equal(t, expectedCarve, carve) +} + +func testCarveBlocks(t *testing.T, ds kolide.Datastore) { + h := test.NewHost(t, ds, "foo.local", "192.168.1.10", "1", "1", time.Now()) + + blockCount := int64(25) + blockSize := int64(30) + carve := &kolide.CarveMetadata{ + HostId: h.ID, + Name: "foobar", + BlockCount: blockCount, + BlockSize: blockSize, + CarveSize: blockCount * blockSize, + CarveId: "carve_id", + RequestId: "request_id", + SessionId: "session_id", + } + + carve, err := ds.NewCarve(carve) + require.NoError(t, err) + + // Randomly generate and insert blocks + expectedBlocks := make([][]byte, blockCount) + for i := int64(0); i < blockCount; i++ { + block := make([]byte, blockSize) + _, err := rand.Read(block) + require.NoError(t, err, "generate block") + expectedBlocks[i] = block + + err = ds.NewBlock(carve.ID, i, block) + require.NoError(t, err, "write block %v", block) + } + + // Verify retrieved blocks match inserted blocks + for i := int64(0); i < blockCount; i++ { + data, err := ds.GetBlock(carve.ID, i) + require.NoError(t, err, "get block %d %v", i, expectedBlocks[i]) + assert.Equal(t, expectedBlocks[i], data) + } + +} + +func testCarveCleanupCarves(t *testing.T, ds kolide.Datastore) { + h := test.NewHost(t, ds, "foo.local", "192.168.1.10", "1", "1", time.Now()) + + blockCount := int64(25) + blockSize := int64(30) + carve := &kolide.CarveMetadata{ + HostId: h.ID, + Name: "foobar", + BlockCount: blockCount, + BlockSize: blockSize, + CarveSize: blockCount * blockSize, + CarveId: "carve_id", + RequestId: "request_id", + SessionId: "session_id", + } + + carve, err := ds.NewCarve(carve) + require.NoError(t, err) + + // Randomly generate and insert blocks + expectedBlocks := make([][]byte, blockCount) + for i := int64(0); i < blockCount; i++ { + block := make([]byte, blockSize) + _, err := rand.Read(block) + require.NoError(t, err, "generate block") + expectedBlocks[i] = block + + err = ds.NewBlock(carve.ID, i, block) + require.NoError(t, err, "write block %v", block) + } + + expired, err := ds.CleanupCarves(time.Now()) + require.NoError(t, err) + assert.Equal(t, 0, expired) + + _, err = ds.GetBlock(carve.ID, 0) + require.NoError(t, err) + + expired, err = ds.CleanupCarves(time.Now().Add(24 * time.Hour)) + require.NoError(t, err) + assert.Equal(t, 1, expired) + + // Should no longer be able to get data + _, err = ds.GetBlock(carve.ID, 0) + require.Error(t, err, "data should be expired") + + carve, err = ds.Carve(carve.ID) + require.NoError(t, err) + assert.True(t, carve.Expired) +} + + +func testCarveListCarves(t *testing.T, ds kolide.Datastore) { + h := test.NewHost(t, ds, "foo.local", "192.168.1.10", "1", "1", time.Now()) + + expectedCarve := &kolide.CarveMetadata{ + HostId: h.ID, + Name: "foobar", + BlockCount: 10, + BlockSize: 12, + CarveSize: 113, + CarveId: "carve_id", + RequestId: "request_id", + SessionId: "session_id", + } + + expectedCarve, err := ds.NewCarve(expectedCarve) + require.NoError(t, err) + assert.NotEqual(t, 0, expectedCarve.ID) + // Add a block to this carve + err = ds.NewBlock(expectedCarve.ID, 0, nil) + require.NoError(t, err) + expectedCarve.MaxBlock = 0 + + expectedCarve2 := &kolide.CarveMetadata{ + HostId: h.ID, + Name: "foobar2", + BlockCount: 42, + BlockSize: 13, + CarveSize: 42 * 13, + CarveId: "carve_id2", + RequestId: "request_id2", + SessionId: "session_id2", + } + + expectedCarve2, err = ds.NewCarve(expectedCarve2) + require.NoError(t, err) + assert.NotEqual(t, 0, expectedCarve2.ID) + expectedCarve2.MaxBlock = -1 + + carves, err := ds.ListCarves(kolide.CarveListOptions{Expired: true}) + require.NoError(t, err) + // Ignore created_at timestamps + expectedCarve.CreatedAt = carves[0].CreatedAt + expectedCarve2.CreatedAt = carves[1].CreatedAt + assert.Equal(t, []*kolide.CarveMetadata{expectedCarve, expectedCarve2}, carves) + + // Expire the carves + _, err = ds.CleanupCarves(time.Now().Add(24 * time.Hour)) + require.NoError(t, err) + + carves, err = ds.ListCarves(kolide.CarveListOptions{Expired: false}) + require.NoError(t, err) + assert.Empty(t, carves) + + carves, err = ds.ListCarves(kolide.CarveListOptions{Expired: true}) + require.NoError(t, err) + assert.Len(t, carves, 2) +} diff --git a/server/datastore/datastore_hosts_test.go b/server/datastore/datastore_hosts_test.go index 67336975f..c49ac1abd 100644 --- a/server/datastore/datastore_hosts_test.go +++ b/server/datastore/datastore_hosts_test.go @@ -7,6 +7,7 @@ import ( "strconv" "testing" "time" + "strings" "github.com/WatchBeam/clock" "github.com/kolide/fleet/server/kolide" @@ -110,7 +111,7 @@ func testDeleteHost(t *testing.T, ds kolide.Datastore) { assert.NotNil(t, err) } -func testListHost(t *testing.T, ds kolide.Datastore) { +func testListHosts(t *testing.T, ds kolide.Datastore) { hosts := []*kolide.Host{} for i := 0; i < 10; i++ { host, err := ds.NewHost(&kolide.Host{ @@ -155,6 +156,41 @@ func testListHost(t *testing.T, ds kolide.Datastore) { require.Equal(t, hosts[0].ID, hosts2[0].ID) } +func testListHostsStatus(t *testing.T, ds kolide.Datastore) { + for i := 0; i < 10; i++ { + _, err := ds.NewHost(&kolide.Host{ + DetailUpdateTime: time.Now(), + LabelUpdateTime: time.Now(), + SeenTime: time.Now().Add(-time.Duration(i) *time.Minute), + OsqueryHostID: strconv.Itoa(i), + NodeKey: fmt.Sprintf("%d", i), + UUID: fmt.Sprintf("%d", i), + HostName: fmt.Sprintf("foo.local%d", i), + }) + assert.Nil(t, err) + if err != nil { + return + } + } + + hosts, err := ds.ListHosts(kolide.HostListOptions{StatusFilter: "online"}) + require.Nil(t, err) + assert.Equal(t, 1, len(hosts)) + + hosts, err = ds.ListHosts(kolide.HostListOptions{StatusFilter: "offline"}) + require.Nil(t, err) + assert.Equal(t, 9, len(hosts)) + + hosts, err = ds.ListHosts(kolide.HostListOptions{StatusFilter: "mia"}) + require.Nil(t, err) + assert.Equal(t, 0, len(hosts)) + + hosts, err = ds.ListHosts(kolide.HostListOptions{StatusFilter: "new"}) + require.Nil(t, err) + assert.Equal(t, 10, len(hosts)) +} + + func testEnrollHost(t *testing.T, ds kolide.Datastore) { test.AddAllHostsLabel(t, ds) var hosts []*kolide.Host @@ -186,6 +222,17 @@ func testAuthenticateHost(t *testing.T, ds kolide.Datastore) { assert.NotNil(t, err) } +func testAuthenticateHostCaseSensitive(t *testing.T, ds kolide.Datastore) { + test.AddAllHostsLabel(t, ds) + for _, tt := range enrollTests { + h, err := ds.EnrollHost(tt.uuid, tt.nodeKey, "default") + require.Nil(t, err) + + _, err = ds.AuthenticateHost(strings.ToUpper(h.NodeKey)) + require.Error(t, err, "node key authentication should be case sensitive") + } +} + func testSearchHosts(t *testing.T, ds kolide.Datastore) { _, err := ds.NewHost(&kolide.Host{ OsqueryHostID: "1234", diff --git a/server/datastore/datastore_packs_test.go b/server/datastore/datastore_packs_test.go index 4cfaec585..ebb226148 100644 --- a/server/datastore/datastore_packs_test.go +++ b/server/datastore/datastore_packs_test.go @@ -25,6 +25,20 @@ func testDeletePack(t *testing.T, ds kolide.Datastore) { assert.NotNil(t, err) } +func testNewPack(t *testing.T, ds kolide.Datastore) { + pack := &kolide.Pack{ + Name: "foo", + } + + pack, err := ds.NewPack(pack) + require.NoError(t, err) + assert.NotEqual(t, uint(0), pack.ID) + + pack, err = ds.Pack(pack.ID) + require.NoError(t, err) + assert.Equal(t, "foo", pack.Name) +} + func testGetPackByName(t *testing.T, ds kolide.Datastore) { pack := test.NewPack(t, ds, "foo") assert.NotEqual(t, uint(0), pack.ID) diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index 482a68229..5552ad4c6 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -10,6 +10,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testOrgInfo, testAdditionalQueries, testEnrollSecrets, + testEnrollSecretsCaseSensitive, testEnrollSecretRoundtrip, testCreateInvite, testInviteByEmail, @@ -22,8 +23,10 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testSaveQuery, testListQuery, testDeletePack, + testNewPack, testEnrollHost, testAuthenticateHost, + testAuthenticateHostCaseSensitive, testLabels, testSaveLabel, testManagingLabelsOnPacks, @@ -40,7 +43,8 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testListUniqueHostsInLabels, testSaveHosts, testDeleteHost, - testListHost, + testListHosts, + testListHostsStatus, testListHostsInPack, testListPacksForHost, testHostIDsByName, @@ -82,4 +86,8 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testLabelIDsByName, testListLabelsForPack, testHostAdditional, + testCarveMetadata, + testCarveBlocks, + testCarveListCarves, + testCarveCleanupCarves, } diff --git a/server/datastore/inmem/hosts.go b/server/datastore/inmem/hosts.go index 6a87993ec..7680478e5 100644 --- a/server/datastore/inmem/hosts.go +++ b/server/datastore/inmem/hosts.go @@ -1,6 +1,7 @@ package inmem import ( + "encoding/json" "errors" "sort" "strings" @@ -103,6 +104,33 @@ func (d *Datastore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error low, high := d.getLimitOffsetSliceBounds(opt.ListOptions, len(hosts)) hosts = hosts[low:high] + // Filter additional info + if len(opt.AdditionalFilters) > 0 { + fieldsWanted := map[string]interface{}{} + for _, field := range opt.AdditionalFilters { + fieldsWanted[field] = true + } + for i, host := range hosts { + addInfo := map[string]interface{}{} + if err := json.Unmarshal(*host.Additional, &addInfo); err != nil { + return nil, err + } + + for k := range addInfo { + if _, ok := fieldsWanted[k]; !ok { + delete(addInfo, k) + } + } + addInfoJSON := json.RawMessage{} + addInfoJSON, err := json.Marshal(addInfo) + if err != nil { + return nil, err + } + host.Additional = &addInfoJSON + hosts[i] = host + } + } + return hosts, nil } diff --git a/server/datastore/mysql/carves.go b/server/datastore/mysql/carves.go new file mode 100644 index 000000000..c4a63aa59 --- /dev/null +++ b/server/datastore/mysql/carves.go @@ -0,0 +1,229 @@ +package mysql + +import ( + "database/sql" + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +func (d *Datastore) NewCarve(metadata *kolide.CarveMetadata) (*kolide.CarveMetadata, error) { + stmt := `INSERT INTO carve_metadata ( + host_id, + name, + block_count, + block_size, + carve_size, + carve_id, + request_id, + session_id + ) VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ? + )` + + result, err := d.db.Exec( + stmt, + metadata.HostId, + metadata.Name, + metadata.BlockCount, + metadata.BlockSize, + metadata.CarveSize, + metadata.CarveId, + metadata.RequestId, + metadata.SessionId, + ) + if err != nil { + return nil, errors.Wrap(err, "insert carve metadata") + } + + id, _ := result.LastInsertId() + metadata.ID = id + + return metadata, nil +} + +func (d *Datastore) CleanupCarves(now time.Time) (int, error) { + var countExpired int + err := d.withRetryTxx(func(tx *sqlx.Tx) error { + // Get IDs of carves to expire + stmt := ` + SELECT id + FROM carve_metadata + WHERE expired = 0 AND created_at < (? - INTERVAL 24 HOUR) + LIMIT 50000 + ` + var expiredCarves []int64 + if err := tx.Select(&expiredCarves, stmt, now); err != nil { + return errors.Wrap(err, "get expired carves") + } + + countExpired = len(expiredCarves) + + if len(expiredCarves) == 0 { + // Nothing to do + return nil + } + + // Delete carve block data + stmt = ` + DELETE FROM carve_blocks + WHERE metadata_id IN (?) + ` + stmt, args, err := sqlx.In(stmt, expiredCarves) + if err != nil { + return errors.Wrap(err, "IN for DELETE FROM carve_blocks") + } + stmt = tx.Rebind(stmt) + if _, err := d.db.Exec(stmt, args...); err != nil { + return errors.Wrap(err, "delete carve blocks") + } + + // Mark metadata expired + stmt = ` + UPDATE carve_metadata + SET expired = 1 + WHERE id IN (?) + ` + stmt, args, err = sqlx.In(stmt, expiredCarves) + if err != nil { + return errors.Wrap(err, "IN for UPDATE carve_metadata") + } + stmt = tx.Rebind(stmt) + if _, err := d.db.Exec(stmt, args...); err != nil { + return errors.Wrap(err, "update carve_metadtata") + } + + return nil + }) + if err != nil { + return 0, err + } + + return countExpired, nil + +} + +// Selecting max_block should be very efficient because MySQL is able to use +// the index metadata and optimizes away the SELECT. +const carveSelectFields = ` + id, + host_id, + created_at, + name, + block_count, + block_size, + carve_size, + carve_id, + request_id, + session_id, + expired, + (SELECT COALESCE(MAX(block_id), -1) FROM carve_blocks WHERE metadata_id = id) AS max_block +` + +func (d *Datastore) Carve(carveId int64) (*kolide.CarveMetadata, error) { + stmt := fmt.Sprintf(` + SELECT %s + FROM carve_metadata + WHERE id = ?`, + carveSelectFields, + ) + + var metadata kolide.CarveMetadata + if err := d.db.Get(&metadata, stmt, carveId); err != nil { + return nil, errors.Wrap(err, "get carve by ID") + } + + return &metadata, nil +} + +func (d *Datastore) CarveBySessionId(sessionId string) (*kolide.CarveMetadata, error) { + stmt := fmt.Sprintf(` + SELECT %s + FROM carve_metadata + WHERE session_id = ?`, + carveSelectFields, + ) + + var metadata kolide.CarveMetadata + if err := d.db.Get(&metadata, stmt, sessionId); err != nil { + return nil, errors.Wrap(err, "get carve by session ID") + } + + return &metadata, nil +} + +func (d *Datastore) CarveByName(name string) (*kolide.CarveMetadata, error) { + stmt := fmt.Sprintf(` + SELECT %s + FROM carve_metadata + WHERE name = ?`, + carveSelectFields, + ) + + var metadata kolide.CarveMetadata + if err := d.db.Get(&metadata, stmt, name); err != nil { + return nil, errors.Wrap(err, "get carve by name") + } + + return &metadata, nil +} + +func (d *Datastore) ListCarves(opt kolide.CarveListOptions) ([]*kolide.CarveMetadata, error) { + stmt := fmt.Sprintf(` + SELECT %s + FROM carve_metadata`, + carveSelectFields, + ) + if !opt.Expired { + stmt += ` WHERE NOT expired ` + } + stmt = appendListOptionsToSQL(stmt, opt.ListOptions) + carves := []*kolide.CarveMetadata{} + if err := d.db.Select(&carves, stmt); err != nil && err != sql.ErrNoRows { + return nil, errors.Wrap(err, "list carves") + } + + return carves, nil +} + +func (d *Datastore) NewBlock(metadataId int64, blockId int64, data []byte) error { + stmt := ` + INSERT INTO carve_blocks ( + metadata_id, + block_id, + data + ) VALUES ( + ?, + ?, + ? + )` + if _, err := d.db.Exec(stmt, metadataId, blockId, data); err != nil { + return errors.Wrap(err, "insert carve block") + } + + return nil +} + +func (d *Datastore) GetBlock(metadataId int64, blockId int64) ([]byte, error) { + stmt := ` + SELECT data + FROM carve_blocks + WHERE metadata_id = ? AND block_id = ? + ` + var data []byte + if err := d.db.Get(&data, stmt, metadataId, blockId); err != nil { + return nil, errors.Wrap(err, "select data") + } + + return data, nil +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index ab1a328b7..5af7ed7ae 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -152,22 +152,75 @@ func (d *Datastore) Host(id uint) (*kolide.Host, error) { } func (d *Datastore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error) { - sql := ` - SELECT * FROM hosts - ` + sql := `SELECT id, + osquery_host_id, + created_at, + updated_at, + detail_update_time, + node_key, + host_name, + uuid, + platform, + osquery_version, + os_version, + build, + platform_like, + code_name, + uptime, + physical_memory, + cpu_type, + cpu_subtype, + cpu_brand, + cpu_physical_cores, + cpu_logical_cores, + hardware_vendor, + hardware_model, + hardware_version, + hardware_serial, + computer_name, + primary_ip_id, + seen_time, + distributed_interval, + logger_tls_period, + config_tls_refresh, + primary_ip, + primary_mac, + label_update_time, + enroll_secret_name, + ` + + // Filter additional info by extracting into a new json object + if len(opt.AdditionalFilters) > 0 { + sql += `JSON_OBJECT( + ` + for _, field := range opt.AdditionalFilters { + sql += fmt.Sprintf(`'%s', JSON_EXTRACT(additional, '$."%s"'), `, field, field) + } + sql = sql[:len(sql)-2] + sql += ` + ) AS additional + ` + } else { + sql += ` + additional + ` + } + + sql += `FROM hosts + ` var params []interface{} switch opt.StatusFilter { case "new": - sql += "AND DATE_ADD(created_at, INTERVAL 1 DAY) >= ?" + sql += "WHERE DATE_ADD(created_at, INTERVAL 1 DAY) >= ?" params = append(params, time.Now()) case "online": - sql += fmt.Sprintf("AND DATE_ADD(seen_time, INTERVAL LEAST(distributed_interval, config_tls_refresh) + %d SECOND) > ?", kolide.OnlineIntervalBuffer) + sql += fmt.Sprintf("WHERE DATE_ADD(seen_time, INTERVAL LEAST(distributed_interval, config_tls_refresh) + %d SECOND) > ?", kolide.OnlineIntervalBuffer) params = append(params, time.Now()) case "offline": - sql += fmt.Sprintf("AND DATE_ADD(seen_time, INTERVAL LEAST(distributed_interval, config_tls_refresh) + %d SECOND) <= ? AND DATE_ADD(seen_time, INTERVAL 30 DAY) >= ?", kolide.OnlineIntervalBuffer) + sql += fmt.Sprintf("WHERE DATE_ADD(seen_time, INTERVAL LEAST(distributed_interval, config_tls_refresh) + %d SECOND) <= ? AND DATE_ADD(seen_time, INTERVAL 30 DAY) >= ?", kolide.OnlineIntervalBuffer) params = append(params, time.Now(), time.Now()) case "mia": - sql += "AND DATE_ADD(seen_time, INTERVAL 30 DAY) <= ?" + sql += "WHERE DATE_ADD(seen_time, INTERVAL 30 DAY) <= ?" params = append(params, time.Now()) } sql = appendListOptionsToSQL(sql, opt.ListOptions) diff --git a/server/datastore/mysql/migrations/tables/20201021104586_CreateCarveTables.go b/server/datastore/mysql/migrations/tables/20201021104586_CreateCarveTables.go new file mode 100644 index 000000000..249061377 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20201021104586_CreateCarveTables.go @@ -0,0 +1,49 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20201021104586, Down_20201021104586) +} + +func Up_20201021104586(tx *sql.Tx) error { + if _, err := tx.Exec(` + CREATE TABLE IF NOT EXISTS carve_metadata ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + host_id INT UNSIGNED NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + name VARCHAR(255), + block_count INT UNSIGNED NOT NULL, + block_size INT UNSIGNED NOT NULL, + carve_size BIGINT UNSIGNED NOT NULL, + carve_id VARCHAR(64) NOT NULL, + request_id VARCHAR(64) NOT NULL, + session_id VARCHAR(64) NOT NULL, + expired TINYINT DEFAULT 0, + UNIQUE KEY idx_session_id (session_id), + UNIQUE KEY idx_name (name), + FOREIGN KEY (host_id) REFERENCES hosts (id) ON DELETE CASCADE + )`); err != nil { + return errors.Wrap(err, "create carve_metadata") + } + + if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS carve_blocks ( + metadata_id INT UNSIGNED NOT NULL, + block_id INT NOT NULL, + data LONGBLOB, + PRIMARY KEY (metadata_id, block_id), + FOREIGN KEY (metadata_id) REFERENCES carve_metadata (id) ON DELETE CASCADE + )`); err != nil { + return errors.Wrap(err, "create carve_blocks") + } + + return nil +} + +func Down_20201021104586(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go b/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go new file mode 100644 index 000000000..b9e6b3285 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go @@ -0,0 +1,35 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20201102112520, Down_20201102112520) +} + +func Up_20201102112520(tx *sql.Tx) error { + query := ` + ALTER TABLE enroll_secrets + MODIFY secret VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "alter enroll secret collation") + } + + query = ` + ALTER TABLE hosts + MODIFY node_key VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "alter node key collation") + } + + return nil +} + +func Down_20201102112520(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 0c06f5abe..83e03e67e 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -223,7 +223,7 @@ func (d *Datastore) NewPack(pack *kolide.Pack, opts ...kolide.OptionalArg) (*kol query := ` INSERT INTO packs (name, description, platform, disabled) - VALUES ( ?, ?, ?, ?, ?) + VALUES ( ?, ?, ?, ? ) ` result, err := db.Exec(query, pack.Name, pack.Description, pack.Platform, pack.Disabled) diff --git a/server/kolide/carves.go b/server/kolide/carves.go new file mode 100644 index 000000000..122087343 --- /dev/null +++ b/server/kolide/carves.go @@ -0,0 +1,83 @@ +package kolide + +import ( + "context" + "time" +) + +type CarveStore interface { + NewCarve(metadata *CarveMetadata) (*CarveMetadata, error) + Carve(carveId int64) (*CarveMetadata, error) + CarveBySessionId(sessionId string) (*CarveMetadata, error) + CarveByName(name string) (*CarveMetadata, error) + ListCarves(opt CarveListOptions) ([]*CarveMetadata, error) + NewBlock(carveId, blockId int64, data []byte) error + GetBlock(carveId, blockId int64) ([]byte, error) + // CleanupCarves will mark carves older than 24 hours expired, and delete the + // associated data blocks. + CleanupCarves(now time.Time) (expired int, err error) +} + +type CarveService interface { + CarveBegin(ctx context.Context, payload CarveBeginPayload) (*CarveMetadata, error) + CarveBlock(ctx context.Context, payload CarveBlockPayload) error + GetCarve(ctx context.Context, id int64) (*CarveMetadata, error) + ListCarves(ctx context.Context, opt CarveListOptions) ([]*CarveMetadata, error) + GetBlock(ctx context.Context, carveId, blockId int64) ([]byte, error) +} + +type CarveMetadata struct { + // ID is the DB auto-increment ID for the carve. + ID int64 `json:"id" db:"id"` + // CreatedAt is the creation timestamp. + CreatedAt time.Time `json:"created_at" db:"created_at"` + // HostId is the ID of the host that initiated the carve. + HostId uint `json:"host_id" db:"host_id"` + // Name is the human readable name for this carve. + Name string `json:"name" db:"name"` + // BlockCount is the number of blocks in the carve. + BlockCount int64 `json:"block_count" db:"block_count"` + // BlcokSize is the size of each block in the carve. + BlockSize int64 `json:"block_size" db:"block_size"` + // CarveSize is the total size of the carve. + CarveSize int64 `json:"carve_size" db:"carve_size"` + // CarveId is a uuid generated by osquery for the carve. + CarveId string `json:"carve_id" db:"carve_id"` + // RequestId is the name of the query that kicked off this carve. + RequestId string `json:"request_id" db:"request_id"` + // SessionId is generated by Fleet and used by osquery to identify blocks. + SessionId string `json:"session_id" db:"session_id"` + // Expired is whether the carve has "expired" (data has been purged). + Expired bool `json:"expired" db:"expired"` + + // MaxBlock is the highest block number currently stored for this carve. + // This value is not stored directly, but generated from the carve_blocks + // table. + MaxBlock int64 `json:"max_block" db:"max_block"` +} + +func (m *CarveMetadata) BlocksComplete() bool { + return m.MaxBlock == m.BlockCount-1 +} + +type CarveListOptions struct { + ListOptions + + // Expired determines whether to include expired carves. + Expired bool +} + +type CarveBeginPayload struct { + BlockCount int64 + BlockSize int64 + CarveSize int64 + CarveId string + RequestId string +} + +type CarveBlockPayload struct { + SessionId string + RequestId string + BlockId int64 + Data []byte +} diff --git a/server/kolide/datastore.go b/server/kolide/datastore.go index 40dcb8a06..8db4e97db 100644 --- a/server/kolide/datastore.go +++ b/server/kolide/datastore.go @@ -15,6 +15,7 @@ type Datastore interface { InviteStore ScheduledQueryStore OsqueryOptionsStore + CarveStore Name() string Drop() error // MigrateTables creates and migrates the table schemas diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index 8a45721ba..30fd2e288 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -87,7 +87,8 @@ type HostService interface { type HostListOptions struct { ListOptions - StatusFilter HostStatus + AdditionalFilters []string + StatusFilter HostStatus } type Host struct { diff --git a/server/kolide/osquery.go b/server/kolide/osquery.go index 4fc79a55a..14c038281 100644 --- a/server/kolide/osquery.go +++ b/server/kolide/osquery.go @@ -20,6 +20,7 @@ type OsqueryService interface { SubmitDistributedQueryResults(ctx context.Context, results OsqueryDistributedQueryResults, statuses map[string]OsqueryStatus) (err error) SubmitStatusLogs(ctx context.Context, logs []json.RawMessage) (err error) SubmitResultLogs(ctx context.Context, logs []json.RawMessage) (err error) + //CarveBegin(ctx context.Context) } // OsqueryDistributedQueryResults represents the format of the results of an diff --git a/server/kolide/service.go b/server/kolide/service.go index 77c3bbb6c..87a2ec63f 100644 --- a/server/kolide/service.go +++ b/server/kolide/service.go @@ -16,4 +16,5 @@ type Service interface { TargetService ScheduledQueryService StatusService + CarveService } diff --git a/server/kolide/users.go b/server/kolide/users.go index 142d77d26..f00c282b8 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -29,12 +29,13 @@ type UserStore interface { // UserService contains methods for managing a Fleet User. type UserService interface { - // NewUser creates a new User from a request Payload. - NewUser(ctx context.Context, p UserPayload) (user *User, err error) + // CreateUserWithInvite creates a new User from a request payload when there is + // already an existing invitation. + CreateUserWithInvite(ctx context.Context, p UserPayload) (user *User, err error) - // NewAdminCreatedUser allows an admin to create a new user without - // first creating and validating invite tokens. - NewAdminCreatedUser(ctx context.Context, p UserPayload) (user *User, err error) + // CreateUser allows an admin to create a new user without first creating + // and validating invite tokens. + CreateUser(ctx context.Context, p UserPayload) (user *User, err error) // User returns a valid User given a User ID. User(ctx context.Context, id uint) (user *User, err error) @@ -104,17 +105,18 @@ type User struct { // UserPayload is used to modify an existing user type UserPayload struct { - Username *string `json:"username,omitempty"` - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` - Admin *bool `json:"admin,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - Password *string `json:"password,omitempty"` - GravatarURL *string `json:"gravatar_url,omitempty"` - Position *string `json:"position,omitempty"` - InviteToken *string `json:"invite_token,omitempty"` - SSOInvite *bool `json:"sso_invite,omitempty"` - SSOEnabled *bool `json:"sso_enabled,omitempty"` + Username *string `json:"username,omitempty"` + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Admin *bool `json:"admin,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Password *string `json:"password,omitempty"` + GravatarURL *string `json:"gravatar_url,omitempty"` + Position *string `json:"position,omitempty"` + InviteToken *string `json:"invite_token,omitempty"` + SSOInvite *bool `json:"sso_invite,omitempty"` + SSOEnabled *bool `json:"sso_enabled,omitempty"` + AdminForcedPasswordReset *bool `json:"admin_forced_password_reset,omitempty"` } // User creates a user from payload. @@ -142,6 +144,9 @@ func (p UserPayload) User(keySize, cost int) (*User, error) { if p.SSOEnabled != nil { user.SSOEnabled = *p.SSOEnabled } + if p.AdminForcedPasswordReset != nil { + user.AdminForcedPasswordReset = *p.AdminForcedPasswordReset + } return user, nil } diff --git a/server/mail/templates/change_email_confirmation.html b/server/mail/templates/change_email_confirmation.html index 36f5cdff0..d7cedfaf6 100644 --- a/server/mail/templates/change_email_confirmation.html +++ b/server/mail/templates/change_email_confirmation.html @@ -66,7 +66,7 @@

Change Confirmation

- Fleet Documentation + Fleet Documentation diff --git a/server/mail/templates/invite_token.html b/server/mail/templates/invite_token.html index 79d384ebf..a0221b63e 100644 --- a/server/mail/templates/invite_token.html +++ b/server/mail/templates/invite_token.html @@ -70,7 +70,7 @@

You Have Been Invited To Fleet!

- Fleet Documentation + Fleet Documentation diff --git a/server/mail/templates/password_reset.html b/server/mail/templates/password_reset.html index 4ac40a1b9..959c5bd44 100644 --- a/server/mail/templates/password_reset.html +++ b/server/mail/templates/password_reset.html @@ -60,7 +60,7 @@

Reset Your Fleet Password...

- Fleet Documentation + Fleet Documentation diff --git a/server/mail/templates/smtp_setup.html b/server/mail/templates/smtp_setup.html index 04a73c7df..c9c693789 100644 --- a/server/mail/templates/smtp_setup.html +++ b/server/mail/templates/smtp_setup.html @@ -58,7 +58,7 @@

Confirmed Fleet SMTP Setup

- Fleet Documentation + Fleet Documentation diff --git a/server/mock/datastore.go b/server/mock/datastore.go index 8bf9612e6..3ca0cc33b 100644 --- a/server/mock/datastore.go +++ b/server/mock/datastore.go @@ -34,6 +34,7 @@ type Store struct { UserStore QueryStore QueryResultStore + CarveStore } func (m *Store) Drop() error { diff --git a/server/mock/datastore_carves.go b/server/mock/datastore_carves.go new file mode 100644 index 000000000..8c3ddb97a --- /dev/null +++ b/server/mock/datastore_carves.go @@ -0,0 +1,94 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import( + "time" + + "github.com/kolide/fleet/server/kolide" +) + +var _ kolide.CarveStore = (*CarveStore)(nil) + +type NewCarveFunc func(c *kolide.CarveMetadata) (*kolide.CarveMetadata, error) + +type CarveFunc func(carveId int64) (*kolide.CarveMetadata, error) + +type ListCarvesFunc func(opt kolide.CarveListOptions) ([]*kolide.CarveMetadata, error) + +type CarveBySessionIdFunc func(sessionId string) (*kolide.CarveMetadata, error) + +type CarveByNameFunc func(name string) (*kolide.CarveMetadata, error) + +type NewBlockFunc func(metadataId int64, blockId int64, data []byte) error + +type GetBlockFunc func(metadataId int64, blockId int64) ([]byte, error) + +type CleanupCarvesFunc func(now time.Time) (expired int, err error) + +type CarveStore struct { + NewCarveFunc NewCarveFunc + NewCarveFuncInvoked bool + + CarveFunc CarveFunc + CarveFuncInvoked bool + + ListCarvesFunc ListCarvesFunc + ListCarvesFuncInvoked bool + + CarveBySessionIdFunc CarveBySessionIdFunc + CarveBySessionIdFuncInvoked bool + + CarveByNameFunc CarveByNameFunc + CarveByNameFuncInvoked bool + + NewBlockFunc NewBlockFunc + NewBlockFuncInvoked bool + + GetBlockFunc GetBlockFunc + GetBlockFuncInvoked bool + + CleanupCarvesFunc CleanupCarvesFunc + CleanupCarvesFuncInvoked bool +} + +func (s *CarveStore) NewCarve(c *kolide.CarveMetadata) (*kolide.CarveMetadata, error) { + s.NewCarveFuncInvoked = true + return s.NewCarveFunc(c) +} + +func (s *CarveStore) Carve(carveId int64) (*kolide.CarveMetadata, error){ + s.CarveFuncInvoked = true + return s.CarveFunc(carveId) +} + +func (s *CarveStore) ListCarves(opt kolide.CarveListOptions) ([]*kolide.CarveMetadata, error) { + s.ListCarvesFuncInvoked = true + return s.ListCarvesFunc(opt) +} + +func (s *CarveStore) CarveBySessionId(sessionId string) (*kolide.CarveMetadata, error) { + s.CarveBySessionIdFuncInvoked = true + return s.CarveBySessionIdFunc(sessionId) +} + +func (s *CarveStore) CarveByName(name string) (*kolide.CarveMetadata, error) { + s.CarveByNameFuncInvoked = true + return s.CarveByNameFunc(name) +} + + +func (s *CarveStore) NewBlock(metadataId int64, blockId int64, data []byte) error { + s.NewBlockFuncInvoked = true + return s.NewBlockFunc(metadataId, blockId, data) +} + +func (s *CarveStore) GetBlock(metadataId int64, blockId int64) ([]byte, error) { + s.GetBlockFuncInvoked = true + return s.GetBlockFunc(metadataId, blockId) +} + +func (s *CarveStore) CleanupCarves(now time.Time) (expired int, err error) { + s.CleanupCarvesFuncInvoked = true + return s.CleanupCarvesFunc(now) +} diff --git a/server/service/client.go b/server/service/client.go index b82b0e9a7..8b91142b9 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -14,12 +14,17 @@ import ( "github.com/pkg/errors" ) +// httpClient interface allows the HTTP methods to be mocked. +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + type Client struct { addr string baseURL *url.URL urlPrefix string token string - http *http.Client + http httpClient insecureSkipVerify bool } @@ -72,7 +77,7 @@ func NewClient(addr string, insecureSkipVerify bool, rootCA, urlPrefix string) ( }, nil } -func (c *Client) doWithHeaders(verb, path string, params interface{}, headers map[string]string) (*http.Response, error) { +func (c *Client) doWithHeaders(verb, path, rawQuery string, params interface{}, headers map[string]string) (*http.Response, error) { var bodyBytes []byte var err error if params != nil { @@ -84,7 +89,7 @@ func (c *Client) doWithHeaders(verb, path string, params interface{}, headers ma request, err := http.NewRequest( verb, - c.url(path).String(), + c.url(path, rawQuery).String(), bytes.NewBuffer(bodyBytes), ) if err != nil { @@ -97,16 +102,16 @@ func (c *Client) doWithHeaders(verb, path string, params interface{}, headers ma return c.http.Do(request) } -func (c *Client) Do(verb, path string, params interface{}) (*http.Response, error) { +func (c *Client) Do(verb, path, rawQuery string, params interface{}) (*http.Response, error) { headers := map[string]string{ "Content-type": "application/json", "Accept": "application/json", } - return c.doWithHeaders(verb, path, params, headers) + return c.doWithHeaders(verb, path, rawQuery, params, headers) } -func (c *Client) AuthenticatedDo(verb, path string, params interface{}) (*http.Response, error) { +func (c *Client) AuthenticatedDo(verb, path, rawQuery string, params interface{}) (*http.Response, error) { if c.token == "" { return nil, errors.New("authentication token is empty") } @@ -117,15 +122,16 @@ func (c *Client) AuthenticatedDo(verb, path string, params interface{}) (*http.R "Authorization": fmt.Sprintf("Bearer %s", c.token), } - return c.doWithHeaders(verb, path, params, headers) + return c.doWithHeaders(verb, path, rawQuery, params, headers) } func (c *Client) SetToken(t string) { c.token = t } -func (c *Client) url(path string) *url.URL { +func (c *Client) url(path, rawQuery string) *url.URL { u := *c.baseURL u.Path = c.urlPrefix + path + u.RawQuery = rawQuery return &u } diff --git a/server/service/client_appconfig.go b/server/service/client_appconfig.go index 858910c45..1365849b3 100644 --- a/server/service/client_appconfig.go +++ b/server/service/client_appconfig.go @@ -10,7 +10,7 @@ import ( // ApplyAppConfig sends the application config to be applied to the Fleet instance. func (c *Client) ApplyAppConfig(payload *kolide.AppConfigPayload) error { - response, err := c.AuthenticatedDo("PATCH", "/api/v1/kolide/config", payload) + response, err := c.AuthenticatedDo("PATCH", "/api/v1/kolide/config", "", payload) if err != nil { return errors.Wrap(err, "PATCH /api/v1/kolide/config") } @@ -38,7 +38,7 @@ func (c *Client) ApplyAppConfig(payload *kolide.AppConfigPayload) error { // GetAppConfig fetches the application config from the server API func (c *Client) GetAppConfig() (*kolide.AppConfigPayload, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/config", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/config", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/config") } @@ -72,7 +72,7 @@ func (c *Client) GetServerSettings() (*kolide.ServerSettings, error) { // GetEnrollSecretSpec fetches the enroll secrets stored on the server func (c *Client) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/enroll_secret", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/enroll_secret", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/enroll_secret") } @@ -102,7 +102,7 @@ func (c *Client) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) { // ApplyEnrollSecretSpec applies the enroll secrets. func (c *Client) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error { req := applyEnrollSecretSpecRequest{Spec: spec} - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/enroll_secret", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/enroll_secret", "", req) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/spec/enroll_secret") } diff --git a/server/service/client_carves.go b/server/service/client_carves.go new file mode 100644 index 000000000..34fc98f9b --- /dev/null +++ b/server/service/client_carves.go @@ -0,0 +1,192 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// ListCarves lists the file carving sessions +func (c *Client) ListCarves(opt kolide.CarveListOptions) ([]*kolide.CarveMetadata, error) { + endpoint := "/api/v1/kolide/carves" + rawQuery := "" + if opt.Expired { + rawQuery = "expired=1" + } + response, err := c.AuthenticatedDo("GET", endpoint, rawQuery, nil) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/carves") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "list carves received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody listCarvesResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get carves response") + } + if responseBody.Err != nil { + return nil, errors.Errorf("get carves: %s", responseBody.Err) + } + + carves := []*kolide.CarveMetadata{} + for _, carve := range responseBody.Carves { + c := carve + carves = append(carves, &c) + } + + return carves, nil +} + +func (c *Client) GetCarve(carveId int64) (*kolide.CarveMetadata, error) { + endpoint := fmt.Sprintf("/api/v1/kolide/carves/%d", carveId) + response, err := c.AuthenticatedDo("GET", endpoint, "", nil) + if err != nil { + return nil, errors.Wrap(err, "GET "+endpoint) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get carve received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + var responseBody getCarveResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode carve response") + } + if responseBody.Err != nil { + return nil, errors.Errorf("get carve: %s", responseBody.Err) + } + + return &responseBody.Carve, nil +} + + +func (c *Client) getCarveBlock(carveId, blockId int64) ([]byte, error) { + path := fmt.Sprintf( + "/api/v1/kolide/carves/%d/block/%d", + carveId, + blockId, + ) + response, err := c.AuthenticatedDo("GET", path, "", nil) + if err != nil { + return nil, errors.Wrapf(err, "GET %s", path) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get carve block received status %d: %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getCarveBlockResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get carve block response") + } + if responseBody.Err != nil { + return nil, errors.Errorf("get carve block: %s", responseBody.Err) + } + + return responseBody.Data, nil +} + +type carveReader struct { + carve kolide.CarveMetadata + bytesRead int64 + curBlock int64 + buffer []byte + client *Client +} + +func newCarveReader(carve kolide.CarveMetadata, client *Client) *carveReader { + return &carveReader{ + carve: carve, + client: client, + bytesRead: 0, + curBlock: 0, + } +} + +func (r *carveReader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + if r.bytesRead >= r.carve.CarveSize { + return 0, io.EOF + } + + // Load data from API if necessary + if len(r.buffer) == 0 { + var err error + r.buffer, err = r.client.getCarveBlock(r.carve.ID, r.curBlock) + if err != nil { + return 0, errors.Wrapf(err, "get block %d", r.curBlock) + } + r.curBlock++ + } + + // Calculate length we can copy + copyLen := len(p) + if copyLen > len(r.buffer) { + copyLen = len(r.buffer) + } + + // Perform copy and clear copied contents from buffer + copy(p, r.buffer[:copyLen]) + r.buffer = r.buffer[copyLen:] + + r.bytesRead += int64(copyLen) + + return copyLen, nil +} + +// ListCarves lists the file carving sessio +func (c *Client) DownloadCarve(id int64) (io.Reader, error) { + path := fmt.Sprintf("/api/v1/kolide/carves/%d", id) + response, err := c.AuthenticatedDo("GET", path, "", nil) + if err != nil { + return nil, errors.Wrapf(err, "GET %s", path) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "download carve received status %d: %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getCarveResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get carve by name response") + } + if responseBody.Err != nil { + return nil, errors.Errorf("get carve by name: %s", responseBody.Err) + } + + reader := newCarveReader(responseBody.Carve, c) + + return reader, nil +} diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index 1ae5a239e..450550077 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -10,7 +10,7 @@ import ( // GetHosts retrieves the list of all Hosts func (c *Client) GetHosts() ([]HostResponse, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/hosts") } @@ -38,7 +38,7 @@ func (c *Client) GetHosts() ([]HostResponse, error) { // HostByIdentifier retrieves a host by the uuid, osquery_host_id, hostname, or // node_key. func (c *Client) HostByIdentifier(identifier string) (*HostResponse, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts/identifier/"+identifier, nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts/identifier/"+identifier, "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/hosts/identifier") } @@ -67,7 +67,7 @@ func (c *Client) HostByIdentifier(identifier string) (*HostResponse, error) { func (c *Client) DeleteHost(id uint) error { verb := "DELETE" path := fmt.Sprintf("/api/v1/kolide/hosts/%d", id) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return errors.Wrapf(err, "%s %s", verb, path) } diff --git a/server/service/client_labels.go b/server/service/client_labels.go index 3df60610c..a87ef805d 100644 --- a/server/service/client_labels.go +++ b/server/service/client_labels.go @@ -13,7 +13,7 @@ import ( // Fleet instance. func (c *Client) ApplyLabels(specs []*kolide.LabelSpec) error { req := applyLabelSpecsRequest{Specs: specs} - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/labels", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/labels", "", req) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/spec/labels") } @@ -43,7 +43,7 @@ func (c *Client) ApplyLabels(specs []*kolide.LabelSpec) error { // GetLabel retrieves information about a label by name func (c *Client) GetLabel(name string) (*kolide.LabelSpec, error) { verb, path := "GET", "/api/v1/kolide/spec/labels/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/labels") } @@ -76,7 +76,7 @@ func (c *Client) GetLabel(name string) (*kolide.LabelSpec, error) { // GetLabels retrieves the list of all Labels. func (c *Client) GetLabels() ([]*kolide.LabelSpec, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/labels", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/labels", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/labels") } @@ -106,7 +106,7 @@ func (c *Client) GetLabels() ([]*kolide.LabelSpec, error) { // DeleteLabel deletes the label with the matching name. func (c *Client) DeleteLabel(name string) error { verb, path := "DELETE", "/api/v1/kolide/labels/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return errors.Wrapf(err, "%s %s", verb, path) } diff --git a/server/service/client_live_query.go b/server/service/client_live_query.go index 854195a34..9096e0fce 100644 --- a/server/service/client_live_query.go +++ b/server/service/client_live_query.go @@ -64,7 +64,7 @@ func (c *Client) LiveQuery(query string, labels []string, hosts []string) (*Live Query: query, Selected: distributedQueryCampaignTargetsByNames{Labels: labels, Hosts: hosts}, } - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/queries/run_by_names", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/queries/run_by_names", "", req) if err != nil { return nil, errors.Wrap(err, "POST /api/v1/kolide/queries/run_by_names") } diff --git a/server/service/client_options.go b/server/service/client_options.go index ab561a471..3a0e20171 100644 --- a/server/service/client_options.go +++ b/server/service/client_options.go @@ -11,7 +11,7 @@ import ( // ApplyOptions sends the osquery options to be applied to the Fleet instance. func (c *Client) ApplyOptions(spec *kolide.OptionsSpec) error { req := applyOsqueryOptionsSpecRequest{Spec: spec} - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/osquery_options", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/osquery_options", "", req) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/spec/osquery_options") } @@ -41,7 +41,7 @@ func (c *Client) ApplyOptions(spec *kolide.OptionsSpec) error { // GetOptions retrieves the configured osquery options. func (c *Client) GetOptions() (*kolide.OptionsSpec, error) { verb, path := "GET", "/api/v1/kolide/spec/osquery_options" - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return nil, errors.Wrap(err, verb+" "+path) } diff --git a/server/service/client_packs.go b/server/service/client_packs.go index f8bc253bb..8fa2268d8 100644 --- a/server/service/client_packs.go +++ b/server/service/client_packs.go @@ -13,7 +13,7 @@ import ( // Fleet instance. func (c *Client) ApplyPacks(specs []*kolide.PackSpec) error { req := applyPackSpecsRequest{Specs: specs} - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/packs", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/packs", "", req) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/spec/packs") } @@ -43,7 +43,7 @@ func (c *Client) ApplyPacks(specs []*kolide.PackSpec) error { // GetPack retrieves information about a pack func (c *Client) GetPack(name string) (*kolide.PackSpec, error) { verb, path := "GET", "/api/v1/kolide/spec/packs/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/packs") } @@ -76,7 +76,7 @@ func (c *Client) GetPack(name string) (*kolide.PackSpec, error) { // GetPacks retrieves the list of all Packs. func (c *Client) GetPacks() ([]*kolide.PackSpec, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/packs", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/packs", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/packs") } @@ -106,7 +106,7 @@ func (c *Client) GetPacks() ([]*kolide.PackSpec, error) { // DeletePack deletes the pack with the matching name. func (c *Client) DeletePack(name string) error { verb, path := "DELETE", "/api/v1/kolide/packs/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return errors.Wrapf(err, "%s %s", verb, path) } diff --git a/server/service/client_queries.go b/server/service/client_queries.go index 721f56b5c..2ab258d13 100644 --- a/server/service/client_queries.go +++ b/server/service/client_queries.go @@ -13,7 +13,7 @@ import ( // Fleet instance. func (c *Client) ApplyQueries(specs []*kolide.QuerySpec) error { req := applyQuerySpecsRequest{Specs: specs} - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/queries", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/queries", "", req) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/spec/queries") } @@ -43,7 +43,7 @@ func (c *Client) ApplyQueries(specs []*kolide.QuerySpec) error { // GetQuery retrieves the list of all Queries. func (c *Client) GetQuery(name string) (*kolide.QuerySpec, error) { verb, path := "GET", "/api/v1/kolide/spec/queries/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return nil, errors.Wrapf(err, "%s %s", verb, path) } @@ -76,7 +76,7 @@ func (c *Client) GetQuery(name string) (*kolide.QuerySpec, error) { // GetQueries retrieves the list of all Queries. func (c *Client) GetQueries() ([]*kolide.QuerySpec, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/queries", nil) + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/queries", "", nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/queries") } @@ -106,7 +106,7 @@ func (c *Client) GetQueries() ([]*kolide.QuerySpec, error) { // DeleteQuery deletes the query with the matching name. func (c *Client) DeleteQuery(name string) error { verb, path := "DELETE", "/api/v1/kolide/queries/"+url.PathEscape(name) - response, err := c.AuthenticatedDo(verb, path, nil) + response, err := c.AuthenticatedDo(verb, path, "", nil) if err != nil { return errors.Wrapf(err, "%s %s", verb, path) } diff --git a/server/service/client_sessions.go b/server/service/client_sessions.go index d4b52d64a..a3c041fb6 100644 --- a/server/service/client_sessions.go +++ b/server/service/client_sessions.go @@ -15,7 +15,7 @@ func (c *Client) Login(email, password string) (string, error) { Password: password, } - response, err := c.Do("POST", "/api/v1/kolide/login", params) + response, err := c.Do("POST", "/api/v1/kolide/login", "", params) if err != nil { return "", errors.Wrap(err, "POST /api/v1/kolide/login") } @@ -50,7 +50,7 @@ func (c *Client) Login(email, password string) (string, error) { // Logout attempts to logout to the current Fleet instance. func (c *Client) Logout() error { - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/logout", nil) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/logout", "", nil) if err != nil { return errors.Wrap(err, "POST /api/v1/kolide/logout") } diff --git a/server/service/client_setup.go b/server/service/client_setup.go index 816bead91..e9ad42a9f 100644 --- a/server/service/client_setup.go +++ b/server/service/client_setup.go @@ -25,7 +25,7 @@ func (c *Client) Setup(email, username, password, org string) (string, error) { KolideServerURL: &c.addr, } - response, err := c.Do("POST", "/api/v1/setup", params) + response, err := c.Do("POST", "/api/v1/setup", "", params) if err != nil { return "", errors.Wrap(err, "POST /api/v1/setup") } diff --git a/server/service/client_targets.go b/server/service/client_targets.go index 90227fdda..b80de14a1 100644 --- a/server/service/client_targets.go +++ b/server/service/client_targets.go @@ -21,7 +21,7 @@ func (c *Client) SearchTargets(query string, selectedHostIDs, selectedLabelIDs [ }, } - response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/targets", req) + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/targets", "", req) if err != nil { return nil, errors.Wrap(err, "POST /api/v1/kolide/targets") } diff --git a/server/service/client_users.go b/server/service/client_users.go new file mode 100644 index 000000000..7c3b18d40 --- /dev/null +++ b/server/service/client_users.go @@ -0,0 +1,39 @@ +package service + +import ( + "encoding/json" + "net/http" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// CreateUser creates a new user, skipping the invitation process. +func (c *Client) CreateUser(p kolide.UserPayload) error { + verb, path := "POST", "/api/v1/kolide/users/admin" + response, err := c.AuthenticatedDo(verb, path, "", p) + if err != nil { + return errors.Wrapf(err, "%s %s", verb, path) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "create user received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody createUserResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode create user response") + } + + if responseBody.Err != nil { + return errors.Errorf("create user: %s", responseBody.Err) + } + + return nil +} diff --git a/server/service/endpoint_carves.go b/server/service/endpoint_carves.go new file mode 100644 index 000000000..a3d92178d --- /dev/null +++ b/server/service/endpoint_carves.go @@ -0,0 +1,177 @@ +package service + +import ( + "context" + + "github.com/go-kit/kit/endpoint" + "github.com/kolide/fleet/server/kolide" +) + +//////////////////////////////////////////////////////////////////////////////// +// Begin File Carve +//////////////////////////////////////////////////////////////////////////////// + +type carveBeginRequest struct { + NodeKey string `json:"node_key"` + BlockCount int64 `json:"block_count"` + BlockSize int64 `json:"block_size"` + CarveSize int64 `json:"carve_size"` + CarveId string `json:"carve_id"` + RequestId string `json:"request_id"` +} + +type carveBeginResponse struct { + SessionId string `json:"session_id"` + Success bool `json:"success,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r carveBeginResponse) error() error { return r.Err } + +func makeCarveBeginEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(carveBeginRequest) + + payload := kolide.CarveBeginPayload{ + BlockCount: req.BlockCount, + BlockSize: req.BlockSize, + CarveSize: req.CarveSize, + CarveId: req.CarveId, + RequestId: req.RequestId, + } + + carve, err := svc.CarveBegin(ctx, payload) + if err != nil { + return carveBeginResponse{Err: err}, nil + } + + return carveBeginResponse{SessionId: carve.SessionId, Success: true}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Receive Block for File Carve +//////////////////////////////////////////////////////////////////////////////// + +type carveBlockRequest struct { + NodeKey string `json:"node_key"` + BlockId int64 `json:"block_id"` + SessionId string `json:"session_id"` + RequestId string `json:"request_id"` + Data []byte `json:"data"` +} + +type carveBlockResponse struct { + Success bool `json:"success,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r carveBlockResponse) error() error { return r.Err } + +func makeCarveBlockEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(carveBlockRequest) + + payload := kolide.CarveBlockPayload{ + SessionId: req.SessionId, + RequestId: req.RequestId, + BlockId: req.BlockId, + Data: req.Data, + } + + err := svc.CarveBlock(ctx, payload) + if err != nil { + return carveBlockResponse{Err: err}, nil + } + + + return carveBlockResponse{Success: true}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Carve +//////////////////////////////////////////////////////////////////////////////// + +type getCarveRequest struct { + ID int64 +} + +type getCarveResponse struct { + Carve kolide.CarveMetadata `json:"carve"` + Err error `json:"error,omitempty"` +} + +func (r getCarveResponse) error() error { return r.Err } + +func makeGetCarveEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getCarveRequest) + carve, err := svc.GetCarve(ctx, req.ID) + if err != nil { + return getCarveResponse{Err: err}, nil + } + + return getCarveResponse{Carve:*carve}, nil + + } +} + +//////////////////////////////////////////////////////////////////////////////// +// List Carves +//////////////////////////////////////////////////////////////////////////////// + +type listCarvesRequest struct { + ListOptions kolide.CarveListOptions +} + +type listCarvesResponse struct { + Carves []kolide.CarveMetadata `json:"carves"` + Err error `json:"error,omitempty"` +} + +func (r listCarvesResponse) error() error { return r.Err } + +func makeListCarvesEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listCarvesRequest) + carves, err := svc.ListCarves(ctx, req.ListOptions) + if err != nil { + return listCarvesResponse{Err: err}, nil + } + + resp := listCarvesResponse{} + for _, carve := range carves { + resp.Carves = append(resp.Carves, *carve) + } + return resp, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Carve Block +//////////////////////////////////////////////////////////////////////////////// + +type getCarveBlockRequest struct { + ID int64 + BlockId int64 +} + +type getCarveBlockResponse struct { + Data []byte `json:"data"` + Err error `json:"error,omitempty"` +} + +func (r getCarveBlockResponse) error() error { return r.Err } + +func makeGetCarveBlockEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getCarveBlockRequest) + data, err := svc.GetBlock(ctx, req.ID, req.BlockId) + if err != nil { + return getCarveBlockResponse{Err: err}, nil + } + + return getCarveBlockResponse{Data: data}, nil + } +} diff --git a/server/service/endpoint_setup.go b/server/service/endpoint_setup.go index 8bd0eabd7..bda7c48e7 100644 --- a/server/service/endpoint_setup.go +++ b/server/service/endpoint_setup.go @@ -57,7 +57,7 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint { err := errors.Errorf("admin password cannot be empty") return setupResponse{Err: err}, nil } - admin, err = svc.NewAdminCreatedUser(ctx, *req.Admin) + admin, err = svc.CreateUser(ctx, *req.Admin) if err != nil { return setupResponse{Err: err}, nil } diff --git a/server/service/endpoint_users.go b/server/service/endpoint_users.go index 2544bcfa1..28126cbb2 100644 --- a/server/service/endpoint_users.go +++ b/server/service/endpoint_users.go @@ -9,7 +9,7 @@ import ( ) //////////////////////////////////////////////////////////////////////////////// -// Create User +// Create User With Invite //////////////////////////////////////////////////////////////////////////////// type createUserRequest struct { @@ -23,10 +23,25 @@ type createUserResponse struct { func (r createUserResponse) error() error { return r.Err } +func makeCreateUserWithInviteEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createUserRequest) + user, err := svc.CreateUserWithInvite(ctx, req.payload) + if err != nil { + return createUserResponse{Err: err}, nil + } + return createUserResponse{User: user}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Create User +//////////////////////////////////////////////////////////////////////////////// + func makeCreateUserEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(createUserRequest) - user, err := svc.NewUser(ctx, req.payload) + user, err := svc.CreateUser(ctx, req.payload) if err != nil { return createUserResponse{Err: err}, nil } diff --git a/server/service/handler.go b/server/service/handler.go index ed8f71d7b..869459563 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -22,6 +22,7 @@ type KolideEndpoints struct { ResetPassword endpoint.Endpoint Me endpoint.Endpoint ChangePassword endpoint.Endpoint + CreateUserWithInvite endpoint.Endpoint CreateUser endpoint.Endpoint GetUser endpoint.Endpoint ListUsers endpoint.Endpoint @@ -73,6 +74,8 @@ type KolideEndpoints struct { GetDistributedQueries endpoint.Endpoint SubmitDistributedQueryResults endpoint.Endpoint SubmitLogs endpoint.Endpoint + CarveBegin endpoint.Endpoint + CarveBlock endpoint.Endpoint CreateLabel endpoint.Endpoint ModifyLabel endpoint.Endpoint GetLabel endpoint.Endpoint @@ -98,20 +101,23 @@ type KolideEndpoints struct { SSOSettings endpoint.Endpoint StatusResultStore endpoint.Endpoint StatusLiveQuery endpoint.Endpoint + ListCarves endpoint.Endpoint + GetCarve endpoint.Endpoint + GetCarveBlock endpoint.Endpoint } // MakeKolideServerEndpoints creates the Kolide API endpoints. func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) KolideEndpoints { return KolideEndpoints{ - Login: makeLoginEndpoint(svc), - Logout: makeLogoutEndpoint(svc), - ForgotPassword: makeForgotPasswordEndpoint(svc), - ResetPassword: makeResetPasswordEndpoint(svc), - CreateUser: makeCreateUserEndpoint(svc), - VerifyInvite: makeVerifyInviteEndpoint(svc), - InitiateSSO: makeInitiateSSOEndpoint(svc), - CallbackSSO: makeCallbackSSOEndpoint(svc, urlPrefix), - SSOSettings: makeSSOSettingsEndpoint(svc), + Login: makeLoginEndpoint(svc), + Logout: makeLogoutEndpoint(svc), + ForgotPassword: makeForgotPasswordEndpoint(svc), + ResetPassword: makeResetPasswordEndpoint(svc), + CreateUserWithInvite: makeCreateUserWithInviteEndpoint(svc), + VerifyInvite: makeVerifyInviteEndpoint(svc), + InitiateSSO: makeInitiateSSOEndpoint(svc), + CallbackSSO: makeCallbackSSOEndpoint(svc, urlPrefix), + SSOSettings: makeSSOSettingsEndpoint(svc), // Authenticated user endpoints // Each of these endpoints should have exactly one @@ -128,6 +134,7 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol AdminUser: authenticatedUser(jwtKey, svc, mustBeAdmin(makeAdminUserEndpoint(svc))), EnableUser: authenticatedUser(jwtKey, svc, mustBeAdmin(makeEnableUserEndpoint(svc))), RequirePasswordReset: authenticatedUser(jwtKey, svc, mustBeAdmin(makeRequirePasswordResetEndpoint(svc))), + CreateUser: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateUserEndpoint(svc))), // PerformRequiredPasswordReset needs only to authenticate the // logged in user PerformRequiredPasswordReset: authenticatedUser(jwtKey, svc, canPerformPasswordReset(makePerformRequiredPasswordResetEndpoint(svc))), @@ -188,6 +195,9 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol GetOsqueryOptionsSpec: authenticatedUser(jwtKey, svc, makeGetOsqueryOptionsSpecEndpoint(svc)), GetCertificate: authenticatedUser(jwtKey, svc, makeCertificateEndpoint(svc)), ChangeEmail: authenticatedUser(jwtKey, svc, makeChangeEmailEndpoint(svc)), + ListCarves: authenticatedUser(jwtKey, svc, makeListCarvesEndpoint(svc)), + GetCarve: authenticatedUser(jwtKey, svc, makeGetCarveEndpoint(svc)), + GetCarveBlock: authenticatedUser(jwtKey, svc, makeGetCarveBlockEndpoint(svc)), // Authenticated status endpoints StatusResultStore: authenticatedUser(jwtKey, svc, makeStatusResultStoreEndpoint(svc)), @@ -199,6 +209,11 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol GetDistributedQueries: authenticatedHost(svc, makeGetDistributedQueriesEndpoint(svc)), SubmitDistributedQueryResults: authenticatedHost(svc, makeSubmitDistributedQueryResultsEndpoint(svc)), SubmitLogs: authenticatedHost(svc, makeSubmitLogsEndpoint(svc)), + CarveBegin: authenticatedHost(svc, makeCarveBeginEndpoint(svc)), + // For some reason osquery does not provide a node key with the block + // data. Instead the carve session ID should be verified in the service + // method. + CarveBlock: makeCarveBlockEndpoint(svc), } } @@ -209,6 +224,7 @@ type kolideHandlers struct { ResetPassword http.Handler Me http.Handler ChangePassword http.Handler + CreateUserWithInvite http.Handler CreateUser http.Handler GetUser http.Handler ListUsers http.Handler @@ -260,6 +276,8 @@ type kolideHandlers struct { GetDistributedQueries http.Handler SubmitDistributedQueryResults http.Handler SubmitLogs http.Handler + CarveBegin http.Handler + CarveBlock http.Handler CreateLabel http.Handler ModifyLabel http.Handler GetLabel http.Handler @@ -285,6 +303,9 @@ type kolideHandlers struct { SettingsSSO http.Handler StatusResultStore http.Handler StatusLiveQuery http.Handler + ListCarves http.Handler + GetCarve http.Handler + GetCarveBlock http.Handler } func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *kolideHandlers { @@ -298,6 +319,7 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), Me: newServer(e.Me, decodeNoParamsRequest), ChangePassword: newServer(e.ChangePassword, decodeChangePasswordRequest), + CreateUserWithInvite: newServer(e.CreateUserWithInvite, decodeCreateUserRequest), CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), GetUser: newServer(e.GetUser, decodeGetUserRequest), ListUsers: newServer(e.ListUsers, decodeListUsersRequest), @@ -349,6 +371,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli GetDistributedQueries: newServer(e.GetDistributedQueries, decodeGetDistributedQueriesRequest), SubmitDistributedQueryResults: newServer(e.SubmitDistributedQueryResults, decodeSubmitDistributedQueryResultsRequest), SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), + CarveBegin: newServer(e.CarveBegin, decodeCarveBeginRequest), + CarveBlock: newServer(e.CarveBlock, decodeCarveBlockRequest), CreateLabel: newServer(e.CreateLabel, decodeCreateLabelRequest), ModifyLabel: newServer(e.ModifyLabel, decodeModifyLabelRequest), GetLabel: newServer(e.GetLabel, decodeGetLabelRequest), @@ -374,6 +398,9 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli SettingsSSO: newServer(e.SSOSettings, decodeNoParamsRequest), StatusResultStore: newServer(e.StatusResultStore, decodeNoParamsRequest), StatusLiveQuery: newServer(e.StatusLiveQuery, decodeNoParamsRequest), + ListCarves: newServer(e.ListCarves, decodeListCarvesRequest), + GetCarve: newServer(e.GetCarve, decodeGetCarveRequest), + GetCarveBlock: newServer(e.GetCarveBlock, decodeGetCarveBlockRequest), } } @@ -427,7 +454,8 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/sso", h.SettingsSSO).Methods("GET").Name("sso_config") r.Handle("/api/v1/kolide/sso/callback", h.CallbackSSO).Methods("POST").Name("callback_sso") r.Handle("/api/v1/kolide/users", h.ListUsers).Methods("GET").Name("list_users") - r.Handle("/api/v1/kolide/users", h.CreateUser).Methods("POST").Name("create_user") + r.Handle("/api/v1/kolide/users", h.CreateUserWithInvite).Methods("POST").Name("create_user_with_invite") + r.Handle("/api/v1/kolide/users/admin", h.CreateUser).Methods("POST").Name("create_user") r.Handle("/api/v1/kolide/users/{id}", h.GetUser).Methods("GET").Name("get_user") r.Handle("/api/v1/kolide/users/{id}", h.ModifyUser).Methods("PATCH").Name("modify_user") r.Handle("/api/v1/kolide/users/{id}/enable", h.EnableUser).Methods("POST").Name("enable_user") @@ -504,11 +532,17 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/status/result_store", h.StatusResultStore).Methods("GET").Name("status_result_store") r.Handle("/api/v1/kolide/status/live_query", h.StatusLiveQuery).Methods("GET").Name("status_live_query") + r.Handle("/api/v1/kolide/carves", h.ListCarves).Methods("GET").Name("list_carves") + r.Handle("/api/v1/kolide/carves/{id}", h.GetCarve).Methods("GET").Name("get_carve") + r.Handle("/api/v1/kolide/carves/{id}/block/{block_id}", h.GetCarveBlock).Methods("GET").Name("get_carve_block") + r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST").Name("enroll_agent") r.Handle("/api/v1/osquery/config", h.GetClientConfig).Methods("POST").Name("get_client_config") r.Handle("/api/v1/osquery/distributed/read", h.GetDistributedQueries).Methods("POST").Name("get_distributed_queries") r.Handle("/api/v1/osquery/distributed/write", h.SubmitDistributedQueryResults).Methods("POST").Name("submit_distributed_query_results") r.Handle("/api/v1/osquery/log", h.SubmitLogs).Methods("POST").Name("submit_logs") + r.Handle("/api/v1/osquery/carve/begin", h.CarveBegin).Methods("POST").Name("carve_begin") + r.Handle("/api/v1/osquery/carve/block", h.CarveBlock).Methods("POST").Name("carve_block") } // WithSetup is an http middleware that checks is setup procedures have been completed. diff --git a/server/service/logging_users.go b/server/service/logging_users.go index e054e7fb8..5a6a4fad3 100644 --- a/server/service/logging_users.go +++ b/server/service/logging_users.go @@ -70,7 +70,7 @@ func (mw loggingMiddleware) ChangeUserEnabled(ctx context.Context, id uint, isEn return user, err } -func (mw loggingMiddleware) NewAdminCreatedUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (mw loggingMiddleware) CreateUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { var ( user *kolide.User err error @@ -85,7 +85,7 @@ func (mw loggingMiddleware) NewAdminCreatedUser(ctx context.Context, p kolide.Us defer func(begin time.Time) { _ = mw.loggerInfo(err).Log( - "method", "NewAdminCreatedUser", + "method", "CreateUser", "user", username, "created_by", loggedInUser, "err", err, @@ -93,7 +93,7 @@ func (mw loggingMiddleware) NewAdminCreatedUser(ctx context.Context, p kolide.Us ) }(time.Now()) - user, err = mw.Service.NewAdminCreatedUser(ctx, p) + user, err = mw.Service.CreateUser(ctx, p) if user != nil { username = user.Username } @@ -151,7 +151,7 @@ func (mw loggingMiddleware) RequirePasswordReset(ctx context.Context, uid uint, } -func (mw loggingMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (mw loggingMiddleware) CreateUserWithInvite(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { var ( user *kolide.User err error @@ -166,7 +166,7 @@ func (mw loggingMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) ( defer func(begin time.Time) { _ = mw.loggerInfo(err).Log( - "method", "NewUser", + "method", "CreateUserWithInvite", "user", username, "created_by", loggedInUser, "err", err, @@ -174,7 +174,7 @@ func (mw loggingMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) ( ) }(time.Now()) - user, err = mw.Service.NewUser(ctx, p) + user, err = mw.Service.CreateUserWithInvite(ctx, p) if user != nil { username = user.Username diff --git a/server/service/metrics_users.go b/server/service/metrics_users.go index 440246b89..157349f75 100644 --- a/server/service/metrics_users.go +++ b/server/service/metrics_users.go @@ -40,19 +40,19 @@ func (mw metricsMiddleware) ChangeUserEnabled(ctx context.Context, id uint, isEn return user, err } -func (mw metricsMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (mw metricsMiddleware) CreateUserWithInvite(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { var ( user *kolide.User err error ) defer func(begin time.Time) { - lvs := []string{"method", "NewUser", "error", fmt.Sprint(err != nil)} + lvs := []string{"method", "CreateUserWithInvite", "error", fmt.Sprint(err != nil)} mw.requestCount.With(lvs...).Add(1) mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) }(time.Now()) - user, err = mw.Service.NewUser(ctx, p) + user, err = mw.Service.CreateUserWithInvite(ctx, p) return user, err } diff --git a/server/service/service_carves.go b/server/service/service_carves.go new file mode 100644 index 000000000..3bc885695 --- /dev/null +++ b/server/service/service_carves.go @@ -0,0 +1,130 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + hostctx "github.com/kolide/fleet/server/contexts/host" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +const ( + maxCarveSize = 8 * 1024 * 1024 * 1024 // 8MB + maxBlockSize = 256 * 1024 * 1024 // 256MB +) + +func (svc service) CarveBegin(ctx context.Context, payload kolide.CarveBeginPayload) (*kolide.CarveMetadata, error) { + host, ok := hostctx.FromContext(ctx) + if !ok { + return nil, osqueryError{message: "internal error: missing host from request context"} + } + + if payload.CarveSize == 0 { + return nil, osqueryError{message: "carve_size must be greater than 0"} + } + + if payload.BlockSize > maxBlockSize { + return nil, osqueryError{message: "block_size exceeds max"} + } + if payload.CarveSize > maxCarveSize { + return nil, osqueryError{message: "carve_size exceeds max"} + } + + // The carve should have a total size that fits appropriately into the + // number of blocks of the specified size. + if payload.CarveSize <= (payload.BlockCount-1)*payload.BlockSize || + payload.CarveSize > payload.BlockCount*payload.BlockSize { + return nil, osqueryError{message: "carve_size does not match block_size and block_count"} + } + + sessionId, err := uuid.NewRandom() + if err != nil { + return nil, osqueryError{message: "internal error: generate session ID for carve: " + err.Error()} + } + + carve := &kolide.CarveMetadata{ + Name: fmt.Sprintf("%s-%s-%s", host.HostName, time.Now().Format(time.RFC3339), payload.RequestId), + HostId: host.ID, + BlockCount: payload.BlockCount, + BlockSize: payload.BlockSize, + CarveSize: payload.CarveSize, + CarveId: payload.CarveId, + RequestId: payload.RequestId, + SessionId: sessionId.String(), + } + + carve, err = svc.ds.NewCarve(carve) + if err != nil { + return nil, osqueryError{message: "internal error: new carve: " + err.Error()} + } + + return carve, nil +} + +func (svc service) CarveBlock(ctx context.Context, payload kolide.CarveBlockPayload) error { + // Note host did not authenticate via node key. We need to authenticate them + // by the session ID and request ID + carve, err := svc.ds.CarveBySessionId(payload.SessionId) + if err != nil { + return errors.Wrap(err, "find carve by session_id") + } + + if payload.RequestId != carve.RequestId { + return fmt.Errorf("request_id does not match") + } + + // Request is now authenticated + + if payload.BlockId > carve.BlockCount-1 { + return fmt.Errorf("block_id exceeds expected max (%d): %d", carve.BlockCount-1, payload.BlockId) + } + + if payload.BlockId != carve.MaxBlock+1 { + return fmt.Errorf("block_id does not match expected block (%d): %d", carve.MaxBlock+1, payload.BlockId) + } + + if int64(len(payload.Data)) > carve.BlockSize { + return fmt.Errorf("exceeded declared block size %d: %d", carve.BlockSize, len(payload.Data)) + } + + if err := svc.ds.NewBlock(carve.ID, payload.BlockId, payload.Data); err != nil { + return errors.Wrap(err, "save block data") + } + + return nil +} + +func (svc service) GetCarve(ctx context.Context, id int64) (*kolide.CarveMetadata, error) { + return svc.ds.Carve(id) +} + +func (svc service) ListCarves(ctx context.Context, opt kolide.CarveListOptions) ([]*kolide.CarveMetadata, error) { + return svc.ds.ListCarves(opt) +} + +func (svc service) GetBlock(ctx context.Context, carveId, blockId int64) ([]byte, error) { + metadata, err := svc.ds.Carve(carveId) + if err != nil { + return nil, errors.Wrap(err, "get carve by name") + } + + if metadata.Expired { + return nil, fmt.Errorf("cannot get block for expired carve") + } + + if blockId > metadata.MaxBlock { + return nil, fmt.Errorf("block %d not yet available", blockId) + } + + data, err := svc.ds.GetBlock(metadata.ID, blockId) + if err != nil { + return nil, errors.Wrapf(err, "get block %d", blockId) + } + + return data, nil +} + + diff --git a/server/service/service_carves_test.go b/server/service/service_carves_test.go new file mode 100644 index 000000000..849403079 --- /dev/null +++ b/server/service/service_carves_test.go @@ -0,0 +1,441 @@ +package service + +import ( + "context" + "fmt" + "testing" + + hostctx "github.com/kolide/fleet/server/contexts/host" + "github.com/kolide/fleet/server/kolide" + "github.com/kolide/fleet/server/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCarveBegin(t *testing.T) { + host := kolide.Host{ID: 3} + payload := kolide.CarveBeginPayload{ + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + } + ms := new(mock.Store) + svc := service{ds: ms} + expectedMetadata := kolide.CarveMetadata{ + ID: 7, + HostId: host.ID, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + } + ms.NewCarveFunc = func(metadata *kolide.CarveMetadata) (*kolide.CarveMetadata, error) { + metadata.ID = 7 + return metadata, nil + } + + ctx := hostctx.NewContext(context.Background(), host) + + metadata, err := svc.CarveBegin(ctx, payload) + require.NoError(t, err) + assert.NotEmpty(t, metadata.SessionId) + metadata.SessionId = "" // Clear this before comparison + metadata.Name = "" // Clear this before comparison + assert.Equal(t, expectedMetadata, *metadata) +} + +func TestCarveBeginNewCarveError(t *testing.T) { + host := kolide.Host{ID: 3} + payload := kolide.CarveBeginPayload{ + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.NewCarveFunc = func(metadata *kolide.CarveMetadata) (*kolide.CarveMetadata, error) { + return nil, fmt.Errorf("ouch!") + } + + ctx := hostctx.NewContext(context.Background(), host) + + _, err := svc.CarveBegin(ctx, payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "ouch!") +} + +func TestCarveBeginEmptyError(t *testing.T) { + ms := new(mock.Store) + svc := service{ds: ms} + ctx := hostctx.NewContext(context.Background(), kolide.Host{}) + + _, err := svc.CarveBegin(ctx, kolide.CarveBeginPayload{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "carve_size must be greater than 0") +} + +func TestCarveBeginMissingHostError(t *testing.T) { + ms := new(mock.Store) + svc := service{ds: ms} + + _, err := svc.CarveBegin(context.Background(), kolide.CarveBeginPayload{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing host") +} + +func TestCarveBeginBlockSizeMaxError(t *testing.T) { + host := kolide.Host{ID: 3} + payload := kolide.CarveBeginPayload{ + BlockCount: 10, + BlockSize: 1024 * 1024 * 1024 * 1024, // 1TB + CarveSize: 10 * 1024 * 1024 * 1024 * 1024, // 10TB + RequestId: "carve_request", + } + ms := new(mock.Store) + svc := service{ds: ms} + + ctx := hostctx.NewContext(context.Background(), host) + + _, err := svc.CarveBegin(ctx, payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "block_size exceeds max") +} + +func TestCarveBeginCarveSizeMaxError(t *testing.T) { + host := kolide.Host{ID: 3} + payload := kolide.CarveBeginPayload{ + BlockCount: 1024 * 1024, + BlockSize: 10 * 1024 * 1024, // 1TB + CarveSize: 10 * 1024 * 1024 * 1024 * 1024, // 10TB + RequestId: "carve_request", + } + ms := new(mock.Store) + svc := service{ds: ms} + + ctx := hostctx.NewContext(context.Background(), host) + + _, err := svc.CarveBegin(ctx, payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "carve_size exceeds max") +} + +func TestCarveBeginCarveSizeError(t *testing.T) { + host := kolide.Host{ID: 3} + payload := kolide.CarveBeginPayload{ + BlockCount: 7, + BlockSize: 13, + CarveSize: 7*13 + 1, + RequestId: "carve_request", + } + ms := new(mock.Store) + svc := service{ds: ms} + ctx := hostctx.NewContext(context.Background(), host) + + // Too big + _, err := svc.CarveBegin(ctx, payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "carve_size does not match") + + // Too small + payload.CarveSize = 6 * 13 + _, err = svc.CarveBegin(ctx, payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "carve_size does not match") +} + +func TestCarveCarveBlockGetCarveError(t *testing.T) { + sessionId := "foobar" + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + return nil, fmt.Errorf("ouch!") + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + SessionId: sessionId, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "ouch!") +} + +func TestCarveCarveBlockRequestIdError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + RequestId: "not_matching", + SessionId: sessionId, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "request_id does not match") +} + +func TestCarveCarveBlockBlockCountExceedError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + RequestId: "carve_request", + SessionId: sessionId, + BlockId: 23, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "block_id exceeds expected max") +} + +func TestCarveCarveBlockBlockCountMatchError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + RequestId: "carve_request", + SessionId: sessionId, + BlockId: 7, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "block_id does not match") +} + +func TestCarveCarveBlockBlockSizeError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 16, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :) TOO LONG!!!"), + RequestId: "carve_request", + SessionId: sessionId, + BlockId: 4, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeded declared block size") +} + +func TestCarveCarveBlockNewBlockError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + ms.NewBlockFunc = func(carveId int64, blockId int64, data []byte) error { + return fmt.Errorf("kaboom!") + } + + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + RequestId: "carve_request", + SessionId: sessionId, + BlockId: 4, + } + + err := svc.CarveBlock(context.Background(), payload) + require.Error(t, err) + assert.Contains(t, err.Error(), "kaboom!") +} + +func TestCarveCarveBlock(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + payload := kolide.CarveBlockPayload{ + Data: []byte("this is the carve data :)"), + RequestId: "carve_request", + SessionId: sessionId, + BlockId: 4, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveBySessionIdFunc = func(sessionId string) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.SessionId, sessionId) + return metadata, nil + } + ms.NewBlockFunc = func(carveId int64, blockId int64, data []byte) error { + assert.Equal(t, metadata.ID, carveId) + assert.Equal(t, int64(4), blockId) + assert.Equal(t, payload.Data, data) + return nil + } + + err := svc.CarveBlock(context.Background(), payload) + require.NoError(t, err) + assert.True(t, ms.NewBlockFuncInvoked) +} + +func TestCarveGetBlock(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveFunc = func(carveId int64) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.ID, carveId) + return metadata, nil + } + ms.GetBlockFunc = func(metadataId int64, blockId int64) ([]byte, error) { + assert.Equal(t, metadata.ID, metadataId) + assert.Equal(t, int64(3), blockId) + return []byte("foobar"), nil + } + + data, err := svc.GetBlock(context.Background(), metadata.ID, 3) + require.NoError(t, err) + assert.Equal(t, []byte("foobar"), data) +} + +func TestCarveGetBlockNotAvailableError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveFunc = func(carveId int64) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.ID, carveId) + return metadata, nil + } + + // Block requested is great than max block + _, err := svc.GetBlock(context.Background(), metadata.ID, 7) + require.Error(t, err) + assert.Contains(t, err.Error(), "not yet available") +} + +func TestCarveGetBlockGetBlockError(t *testing.T) { + sessionId := "foobar" + metadata := &kolide.CarveMetadata{ + ID: 2, + HostId: 3, + BlockCount: 23, + BlockSize: 64, + CarveSize: 23 * 64, + RequestId: "carve_request", + SessionId: sessionId, + MaxBlock: 3, + } + ms := new(mock.Store) + svc := service{ds: ms} + ms.CarveFunc = func(carveId int64) (*kolide.CarveMetadata, error) { + assert.Equal(t, metadata.ID, carveId) + return metadata, nil + } + ms.GetBlockFunc = func(metadataId int64, blockId int64) ([]byte, error) { + assert.Equal(t, metadata.ID, metadataId) + assert.Equal(t, int64(3), blockId) + return nil, fmt.Errorf("yow!!") + } + + // Block requested is great than max block + _, err := svc.GetBlock(context.Background(), metadata.ID, 3) + require.Error(t, err) + assert.Contains(t, err.Error(), "yow!!") +} + + diff --git a/server/service/service_users.go b/server/service/service_users.go index 0836e275f..1e2260fd1 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" ) -func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (svc service) CreateUserWithInvite(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { invite, err := svc.VerifyInvite(ctx, *p.InviteToken) if err != nil { return nil, err @@ -34,17 +34,17 @@ func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.U return user, nil } -func (svc service) NewAdminCreatedUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (svc service) CreateUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { return svc.newUser(p) } func (svc service) newUser(p kolide.UserPayload) (*kolide.User, error) { var ssoEnabled bool // if user is SSO generate a fake password - if p.SSOInvite != nil && *p.SSOInvite { + if (p.SSOInvite != nil && *p.SSOInvite) || (p.SSOEnabled != nil && *p.SSOEnabled) { fakePassword, err := generateRandomText(14) if err != nil { - return nil, err + return nil, errors.Wrap(err, "generate stand-in password") } p.Password = &fakePassword ssoEnabled = true diff --git a/server/service/service_users_test.go b/server/service/service_users_test.go index 97a9493b3..c996f9337 100644 --- a/server/service/service_users_test.go +++ b/server/service/service_users_test.go @@ -335,13 +335,13 @@ func TestRequestPasswordReset(t *testing.T) { } } -func TestCreateUser(t *testing.T) { +func TestCreateUserWithInvite(t *testing.T) { ds, _ := inmem.New(config.TestConfig()) svc, _ := newTestService(ds, nil, nil) invites := setupInvites(t, ds, []string{"admin2@example.com"}) ctx := context.Background() - var createUserTests = []struct { + var newUserTests = []struct { Username *string Password *string Email *string @@ -400,7 +400,7 @@ func TestCreateUser(t *testing.T) { }, } - for _, tt := range createUserTests { + for _, tt := range newUserTests { t.Run("", func(t *testing.T) { payload := kolide.UserPayload{ Username: tt.Username, @@ -409,7 +409,7 @@ func TestCreateUser(t *testing.T) { Admin: tt.Admin, InviteToken: tt.InviteToken, } - user, err := svc.NewUser(ctx, payload) + user, err := svc.CreateUserWithInvite(ctx, payload) if tt.wantErr != nil { require.Equal(t, tt.wantErr.Error(), err.Error()) } diff --git a/server/service/transport_carves.go b/server/service/transport_carves.go new file mode 100644 index 000000000..cb3187a4b --- /dev/null +++ b/server/service/transport_carves.go @@ -0,0 +1,71 @@ +package service + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/pkg/errors" + "github.com/kolide/fleet/server/kolide" +) + +func decodeCarveBeginRequest(ctx context.Context, r *http.Request) (interface{}, error) { + defer r.Body.Close() + + var req carveBeginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(err, "decoding JSON") + } + + return req, nil +} + +func decodeCarveBlockRequest(ctx context.Context, r *http.Request) (interface{}, error) { + defer r.Body.Close() + + var req carveBlockRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(err, "decoding JSON") + } + + return req, nil +} + +func decodeGetCarveRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + return getCarveRequest{ID: int64(id)}, nil +} + +func decodeListCarvesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + copt := kolide.CarveListOptions{ListOptions: opt} + expired := r.URL.Query().Get("expired") + switch expired { + case "1", "true": + copt.Expired = true + case "0", "": + copt.Expired = false + default: + return nil, errors.Errorf("invalid expired value %s", expired) + } + return listCarvesRequest{ListOptions: copt}, nil +} + +func decodeGetCarveBlockRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + blockId, err := idFromRequest(r, "block_id") + if err != nil { + return nil, err + } + return getCarveBlockRequest{ID: int64(id), BlockId: int64(blockId)}, nil +} + diff --git a/server/service/transport_hosts.go b/server/service/transport_hosts.go index 87be34a37..6b5e5135e 100644 --- a/server/service/transport_hosts.go +++ b/server/service/transport_hosts.go @@ -3,6 +3,7 @@ package service import ( "context" "net/http" + "strings" "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" @@ -48,5 +49,10 @@ func decodeListHostsRequest(ctx context.Context, r *http.Request) (interface{}, if err != nil { return nil, err } + + additionalInfoFiltersString := r.URL.Query().Get("additional_info_filters") + if additionalInfoFiltersString != "" { + hopt.AdditionalFilters = strings.Split(additionalInfoFiltersString, ",") + } return listHostsRequest{ListOptions: hopt}, nil } diff --git a/server/service/validation_users.go b/server/service/validation_users.go index a9ed2f279..e331e238f 100644 --- a/server/service/validation_users.go +++ b/server/service/validation_users.go @@ -10,7 +10,7 @@ import ( "github.com/kolide/fleet/server/kolide" ) -func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { +func (mw validationMiddleware) CreateUserWithInvite(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { invalid := &invalidArgumentError{} if p.Username == nil { invalid.Append("username", "missing required argument") @@ -57,7 +57,51 @@ func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload if invalid.HasErrors() { return nil, invalid } - return mw.Service.NewUser(ctx, p) + return mw.Service.CreateUserWithInvite(ctx, p) +} + +func (mw validationMiddleware) CreateUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { + invalid := &invalidArgumentError{} + if p.Username == nil { + invalid.Append("username", "missing required argument username") + } else { + if *p.Username == "" { + invalid.Append("username", "username cannot be empty") + } + + if strings.Contains(*p.Username, "@") { + invalid.Append("username", "'@' character not allowed in usernames") + } + } + + // we don't need a password for single sign on + if (p.SSOInvite == nil || !*p.SSOInvite) && (p.SSOEnabled == nil || !*p.SSOEnabled) { + if p.Password == nil { + invalid.Append("password", "missing required argument password") + } else { + if *p.Password == "" { + invalid.Append("password", "password cannot be empty") + } + // Skip password validation in the case of admin created users + } + } + + if p.Email == nil { + invalid.Append("email", "missing required argument email") + } else { + if *p.Email == "" { + invalid.Append("email", "email cannot be empty") + } + } + + if p.InviteToken != nil { + invalid.Append("invite_token", "invite_token should not be specified with admin user creation") + } + + if invalid.HasErrors() { + return nil, invalid + } + return mw.Service.CreateUser(ctx, p) } func (mw validationMiddleware) ModifyUser(ctx context.Context, userID uint, p kolide.UserPayload) (*kolide.User, error) { diff --git a/tools/osquery/example_osquery.flags b/tools/osquery/example_osquery.flags index 9f868c7ac..90891502b 100644 --- a/tools/osquery/example_osquery.flags +++ b/tools/osquery/example_osquery.flags @@ -23,3 +23,8 @@ --logger_plugin=tls --logger_tls_endpoint=/api/v1/osquery/log --logger_tls_period=10 + +--disable_carver=false +--carver_start_endpoint=/api/v1/osquery/carve/begin +--carver_continue_endpoint=/api/v1/osquery/carve/block +--carver_block_size=2000000 diff --git a/tools/osquery/kolide.crt b/tools/osquery/kolide.crt index f617219e2..f0cf32198 100644 --- a/tools/osquery/kolide.crt +++ b/tools/osquery/kolide.crt @@ -1,19 +1,29 @@ -----BEGIN CERTIFICATE----- -MIIDBjCCAe4CCQDjM7ghDw9OgDANBgkqhkiG9w0BAQsFADBFMQ4wDAYDVQQKDAVG -bGVldDEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxHTAbBgNVBAMMFGhvc3QuZG9ja2Vy -LmludGVybmFsMB4XDTE5MDMyNjE5MTQyN1oXDTQ2MDgxMDE5MTQyN1owRTEOMAwG -A1UECgwFRmxlZXQxFDASBgNVBAsMC0RldmVsb3BtZW50MR0wGwYDVQQDDBRob3N0 -LmRvY2tlci5pbnRlcm5hbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AOnDxrXtuB3VpmsExCVXboF0Llcfee4FF4Y0ifS75RjnLSXupZubQWHenBGmQ7qb -3eTBuw59HiTBkB5CZb79FpFyYrxMpXn+7dVJt2N9NnpKkVidqFXaVqTS9P84JQtt -LIPzyQlRFM482Y0RVfY1UnrWUTqze/wUpJP6xY+aPseIbnYvbsGtZ8PjrLMN+6z8 -f2Ef8Hr6uIR2mQe+qu43HuEkjbTnRi1ORj2fr4cSgoVd7FMhIy1yGEaUP4aerB3X -k1h4eOJa/Vd2eE3b3vXg7awAtdPJDWz4LjQCkgzeI5tej+TooAHM0gyun7EPSy+9 -2fgkAkEvY/dco6h+D0ULuNECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAEhGq1t5G -ZyhXEYJrd92HHi6zyRQNlAoQ55utmE/tlNAq1e2k1uEKEeOmIFvaLpQ3ky2z1Aok -Qrn4+TYFcNN6vB99QxNVb3+hDm+hyKj0N7VvvYIX8ms/AUKZUaUahxM4gXDNky35 -l33IYgGmdcSzBiTkTCGABmtHwJ/QAPI1v6iWGXLQ42HNfCCflFHt5AtOs7Nfv0u5 -JsgkMVu+Vj9sOcdE+UtAJ/NQyV+kWFgzUtxdX8QX/NQVFqlNPVV4cRi8KMbBM1q+ -xN2o1vC27vM+5VWTr5sddU6MVJJXIIP4wfe+qbCnD8GE5+7j8XiUrOMsauiafaW2 -cdSv0nZ/WcEovw== +MIIE5jCCAs6gAwIBAgIJAKq0+FAVArUhMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV +BAMMCkZsZWV0IFRlc3QwHhcNMjAxMDE1MTg1ODE5WhcNMzAwNzE1MTg1ODE5WjAV +MRMwEQYDVQQDDApGbGVldCBUZXN0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAttQ62lpMq48/XjQFxYg47D2fgKgTBMjDNSfCt9VpqE3xPnybmWo8VZtk +jmrFM50IhagyYSjvl9iqdrnsl3ZV8KYbWEy6849zDYF1SudmC7/pJyH7QvpKgL7V +4McM62jM905hyFy9KZTAlFiaeezWSWJre7kHsK2u5tsqS6ElatEZmF59sInixaRw +RqVxOhtDm7Sl1c5xKWM6phWoJfykspFpu5J6N2jRQXCfzBYoQWN76OohGouit/BQ +C1xvm+f7wgGZgbbfDjUoHAe9Yhd3XzZsYTgMDt/SRJRDnxFZwo8BAkY9yJm7f3dQ +AEhgJ66KbyoxITdgma1hgmeWibZY4hVymcRxB1B3RNN2at19RNy2J+brMxlG0KZk +nD77EqidrwLAlYcdeU3yLjt0vYPxT+RW7l1jiZlVi/oaykAmVfOhWnTnTwbsYs7O +UMyMyYHQECEs98ex7wrjThIBJScqhsSN1ipAxr5RgaDr+U5IR+tLhMewBy8So+nf +2YuMhLfkCgoY80ELhz5F8avts5hksB0hqnNYr+Nlwm6eXqEPZSzFJmdc1IbmWzq2 +7UH1OQmBFF2qr2j/8dcM+oPNgjrEEQjtyW0S4j2PhjSEbINgcwu0AaABssLI80Vm +Gp1TjUGA92rMwIjlAtcUUB5FOKSS8vAXb1VcDWMkybh9sHj4Z0ECAwEAAaM5MDcw +NQYDVR0RBC4wLIIJbG9jYWxob3N0ghRob3N0LmRvY2tlci5pbnRlcm5hbIIJMTI3 +LjAuMC4xMA0GCSqGSIb3DQEBCwUAA4ICAQBZOY++LNRTVG8XlQGVlOloEKA2WY3P +gXKJLSM7xWSxj2co0v+noyhoInHT7ysub8en59Et+vN53/0OobxNIdEKDUdqs38R +F++Oy6s/MhFHSo87F06t+W91/60ub4iFRHfev1qeNFV6Yzv9sFJ5LpXLFk+qVDb8 +pPyFFE1bXjctDPjD5gUj+Y34XikVKzMb7xddWCNs34v1KCaCBW7kkfefxiZiDR6g +lCEkDzp6xaLS898oCbfFakjr4bvOgBP1IqXLIDLPMhivaxNAooHTtu/3ezp7puix +TSDkjlkStDtEFw/wjyaMcEkk51Gs1ponBbADLRxQ50AHDWk/4vy8GcIVc6CdVEOA +Zw12FN06C4Jviiiv6uCXZ6iZ+V+pjGiGmSNYF+kruUs8BfrJIB89lqxpdQ4Kx01j +AuSFvjRRvIPmvApSdKEjLcY3AYRivXsB/hASMBbjh/p1f/JzSJdxoqSvONhNQJuh ++wcdNVQhGAv3kkLn/HMHTBl2Ur+9tQaJrnR1tWl1IzwLRJIi0Soyp/q5ZjQyFglj +32xW83DZhtpQ2SI1QGy4AvWIPnGHZhMfav02KnKRhZdOMW4oekXRMrwiyXCqIazc +xXzAlCq8dHdP2Y9uvfFxVFyE+uSfkcPxX+DG/ZnpgCS27oKA/qLCybJamlqtveNs +RSjNe5qwGi0ifA== -----END CERTIFICATE----- diff --git a/tools/osquery/kolide.key b/tools/osquery/kolide.key index e5024c873..f8ac67b1e 100644 --- a/tools/osquery/kolide.key +++ b/tools/osquery/kolide.key @@ -1,28 +1,52 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDpw8a17bgd1aZr -BMQlV26BdC5XH3nuBReGNIn0u+UY5y0l7qWbm0Fh3pwRpkO6m93kwbsOfR4kwZAe -QmW+/RaRcmK8TKV5/u3VSbdjfTZ6SpFYnahV2lak0vT/OCULbSyD88kJURTOPNmN -EVX2NVJ61lE6s3v8FKST+sWPmj7HiG52L27BrWfD46yzDfus/H9hH/B6+riEdpkH -vqruNx7hJI2050YtTkY9n6+HEoKFXexTISMtchhGlD+Gnqwd15NYeHjiWv1XdnhN -29714O2sALXTyQ1s+C40ApIM3iObXo/k6KABzNIMrp+xD0svvdn4JAJBL2P3XKOo -fg9FC7jRAgMBAAECggEAPPViohqFgrIuHYY2qA4usO9dDjcVEq6dPdABkaJV0bKn -IVckNHm+CQmF5UxYKRdwO7ERWGvkKm2XaWyjH7Tb573OZZAPLsr9tjrs6bLDPAyp -7lPei9TN91lTriIz9tDXZOlzqUxNgqJ3kMPOiM/K3GF6+TXIxSmU6UnhrtroOBuq -K+o/qC5K8CnqpBhBQt6qtn/YdfbcABogRbwojnHRl068nMV9kJLp7/I5hgI8Vr+v -xMXI87wbL7kglcBZweG2EL//C31eSEVG2cW+0cIefQil0ZIPWgX0z5Oex2OD+cLL -E+FIp/waZ7plCiikNNMo+gm/4uy9iupxg+z5W71NyQKBgQD2OSTe8w2Ns9zzLIXC -5ZrFTa5LmTJ8bIv7/5kCS7fSGnoDPZvaRfnqzVEoCUp5D2N09NOJIsoNGrIGnMCc -q6HwGEzGwmOgtzYu1r2jC+6jlcJW4tMr8k52CRO3y3LvsYkmt6tYIBgMVoxIupce -QyBm5zVccyIvdhYhapgcSTJrtwKBgQDzC/30lTlBWsLiqLtfHAwM3YJgdVczJrbe -WDieUd8ywTVupDOhBSGwVGmXGLQE5G/eNfwlsBq2iij7H/WdkX6mD1YmnQJUit3x -0cPmDwWMo3vGLUUUI34WJ/2WSxdTtfaIcaFlV6S9Bp9B5K208kG/cZliqBWG1mtS -a/AdsdcPtwKBgFHan2pKzNet0qc4xuMK54/uCiJxtHnur/6/cwNzXpHHYYaQRa6j -kri/NtqjdBGYzwyDk4tEeH0wwbw3AkVTPYuO2H8/AlXccnPRyctZXSUe1TODRoaW -kATncZmpVfVfROQNLTYnvTbk0tAez7wsvqnW9UNdtyCmFidXw2er4cozAoGADuPK -MCTAODfGPrqVmBMQzez4Is6tg+24QFDpTxG2+dYKXvfiTdgRo0rYmTAjPzV7gQKP -qwNp74rxTck8c+XI+4VvAriVvvYu+LSgKsT60w3k9FQrqjsua08R4xZAnJlGPD+x -4pKG/imcsh2/YpmA26iq+/dOMk+KjacdM8SEZ2sCgYB/qypw7LpskkGA5ITODv5A -cAHVCCDMGNJiR4XpdOwHNAcd6EWjB/366/MwKfV24KnZH0RZe+pU2e4dP8miHNN0 -dFOpAokCD3Eh7LGDVM6bYkQabG3XTmN8g9a4V2e63n5Bl2HZx0Qp1bHEtqsOujpj -5G5maHPlTRoNlvk4BiEZDA== +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC21DraWkyrjz9e +NAXFiDjsPZ+AqBMEyMM1J8K31WmoTfE+fJuZajxVm2SOasUznQiFqDJhKO+X2Kp2 +ueyXdlXwphtYTLrzj3MNgXVK52YLv+knIftC+kqAvtXgxwzraMz3TmHIXL0plMCU +WJp57NZJYmt7uQewra7m2ypLoSVq0RmYXn2wieLFpHBGpXE6G0ObtKXVznEpYzqm +Fagl/KSykWm7kno3aNFBcJ/MFihBY3vo6iEai6K38FALXG+b5/vCAZmBtt8ONSgc +B71iF3dfNmxhOAwO39JElEOfEVnCjwECRj3Imbt/d1AASGAnropvKjEhN2CZrWGC +Z5aJtljiFXKZxHEHUHdE03Zq3X1E3LYn5uszGUbQpmScPvsSqJ2vAsCVhx15TfIu +O3S9g/FP5FbuXWOJmVWL+hrKQCZV86FadOdPBuxizs5QzIzJgdAQISz3x7HvCuNO +EgElJyqGxI3WKkDGvlGBoOv5TkhH60uEx7AHLxKj6d/Zi4yEt+QKChjzQQuHPkXx +q+2zmGSwHSGqc1iv42XCbp5eoQ9lLMUmZ1zUhuZbOrbtQfU5CYEUXaqvaP/x1wz6 +g82COsQRCO3JbRLiPY+GNIRsg2BzC7QBoAGywsjzRWYanVONQYD3aszAiOUC1xRQ +HkU4pJLy8BdvVVwNYyTJuH2wePhnQQIDAQABAoICAHY0mSh7oX56SzoY4HJqNINp +BCsmf8VkF/HSFy7MKFfMrOD9p4x1BzVCFq2NkppgORZRPOFcmivOxcelRbZXqBDD +FILueSDn6jcdMDyRh4SpO5E1g0I2mNzMhXOivlOlmn96ifYFH7g2mJxQ/O8/lPw1 +SdDgPHyajq5rnNjxA0QglS/SR+lP/PJT8tN6O86Zugh9r4qgwsFDirO+5MxKoCFj +qry/Y41Xk5N/wZEt8jD0pTRdy7Fh/n52PQpbZT2jqz4v0pPvLB//dkNJLcSx71f7 +0+63UfR7+XKleWLq0OHdm4Vg7Kk52+P5vBuyr5h4XMCqwsD7ENXLJ3QsjUPURXot +RWHy2oqVLb2/zFpYTc/pLtIkJtkgZB9kq7O7+kjlO/k8FeKjHU109ERWS2Wj2md+ +hWLXxlYAT9XJmKQAtt7BsKWw1XncO+9TCZ1NDmRLr4oAfLF+nQ8r9N/Uy2Grk82G +cY3mRrGADS8ioX4DXvxxbsDu8iPAfcoN/wKtZ+cTBIaUW8qaaBHavbl+ZPi/HXo0 +sGFLgMApHlygqAmY5LEXs7lz7psh8MwbDllHHipABmgttWBQ3qSdpb/dTw4eyHX/ +9QlEF5MzL/SqcMJsfh1NVhYoOKrroDwyS96eFtNYs8lpiYzf18Wm/69FtdWVTZDL +EpVc+dQsRvgBzlVQDBARAoIBAQDzD6l/zcqKKUATG9WgbVXK3E6q6srx2csE+HCI +OAa9dq893YX35Z6DNZdE95NxO2OG+iAb4kSl498uOct0nKIdWF8VsSPvWN5igjaM +r3E/7JQE6nRLdyRDN+m4KGJCpmSoGhs9SjyjRUs7mLyxzNpADfpWmNo1MQFGSlUy +W8xD2nrB0BZiLFHCntMVhOZXFenjqmG1KVA1IsrtLebEwDrLeMEXH9miNAFKnYgf +W3b42MWDdfUqbgAQFtloiuwQRItvf4frrXoo++pYTPZlROHK0wQZcRG33eMvDhYd +XjDF0dvIzrGKikj64Xf3rXXUQT7zKQ2PdZokK7Q6mQOzF/h/AoIBAQDAj7+qw5aU +nZtNSFLx2pihe4d75JVBCQVJlNEpl0A3+UKpp/r8xGG57iXxZC0ZNhWWfN3u5TaB +32d6IbqQT8cSDa4Z9EwYABoSGcj1xVCPS9PJqkxpwC7AlpwYC4Vk5++Wt85nujIx +rQYit6V3WxHtxtfD/RVTMWUWUnMjmMGSGneFXT3jLn+7Hh2rITNPuM+KMSPK3D3c +I6j+SBJanaXMTfWqFyPs6fJuku2rVx8kivfZz1XLDv/K0Fjp3pZfVebYLH2Heyk6 +0FElVksLbGUUZCvdI02d4ds4DmZQzNMfDrk9x05TnH/RlM2hNYCslNmpDTdc4C8b +VP0s2PWSzcA/AoIBAQC88lXvVgi28m4u7JpaxePFeRFmaHMvIwFhccf4/KSwsAx2 +aTh1hvh8QzK+mD7z7RjnmpVPV8vJsaSTCLaShhi3+zSfZj7rFwh5R4QkRVYiPUSr +tZ6F864q2tJDKJGKAlOJIhI+yPDuczWStJ8rEHYxCSysGNK4Osok3C/yn83giXyY +Iazk0FMWmeS5e8Cbvhs9sfuWmvEQ/WUGj4IvPMf32H0x/r5uC0Ndv6xLxAxuUgTo +ts/JFg3SdBC55hSwaLiECn2cxhSKu5pm3h1EiBGGXBd5t53wcvriX27tkYUUopja +N2NosSsebBuYXC2WvMm0uRsjhGY47AiE7OIlXOL9AoIBAA04EQ1U/fpX03h7tY36 +1q2HQGbF62UajG1ftPgo9PSivOvOp1FA3gCYk6w9l0b6yoKZMdcbjyFdR8o/lIIh +p+XaYJBRkAO5xhBmCsCsefpMV06pzTMZSVxZOoAAEnk53t224omGY7m7SgeKGebW +rqVnGBrRPeyHIIxmvpa7/tXb0Uarfvsgjw42ZA0Ca3ZWlpDDDNT5R5ncLNa/9dqY +pfAfjfTOP94ctVLX7U7s3StyCs++BwK4leDDascrS6Fh0UYXz8pELzFlaZypjt9K +4qmCsuwsZ0CmZ5kyi92SIhAov5i5HOxqeu/VSkR92sZ+NW8AhENw9Gro67RvdHRO +gZsCggEBAIjpsGAL1P4zmXW07EWfOKSmQmfusl/hbaKl2FKaM5rtku73GjW2kwBw +0RE/cKqENWasAryYPaDjGys+c6xzrYS3Mo2FT5vP6jRl0TxPsQjE1JY2wNgXbHss +y1Afjumv5vHBLaSKm0v3UF57NCndKWTebBURY4V1CiXetOaR2hNgbI6zPSNP2srz +Cphr4i3O2H98wKhC6mmYA46zF1imkFu5ZYBEnue4pHPMkyGzwqrvLRmahIr+C313 +5rW3mumKvHHGbMYJCQVdM7edxZJ4Qn3gIAlJlOQtcPyqVwuwyXnVybTIsnhRiHP8 +mWZen+G8ANnds10V7TmcpoaPSSoJMF0= -----END PRIVATE KEY----- diff --git a/www.fleetdm.com/README.md b/www.fleetdm.com/README.md new file mode 100644 index 000000000..a99ae3dae --- /dev/null +++ b/www.fleetdm.com/README.md @@ -0,0 +1,5 @@ +# www.fleetdm.com + +This is where the code for the public https://fleetdm.com website will live. + +Soon. But for now, it's actually here: https://github.com/fleetdm/fleetdm.com \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7f82b61d9..766588af0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,6 +219,11 @@ accepts@~1.3.5: mime-types "~2.1.24" negotiator "0.6.2" +ace-builds@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.3.1.tgz#c7f9d7a657e7d9c630acd78f1dc2fa1e0e2a84f6" + integrity sha512-MJtPAqeGaiIpfgUCXi3/oowqcIw4wSkKTDGvtfUoQHrfZGfjNnH3frPdHzd1VfKF62JFeNJOl4q0TRDiHwoBFg== + acorn-globals@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"