diff --git a/.circleci/config.yml b/.circleci/config.yml index 88100ba74..30e296950 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,12 +12,6 @@ jobs: - vendor-cache-{{ .Branch }} - vendor-cache - - run: - name: "fix node-sass" - command: | - yarn remove node-sass - yarn add node-sass@latest - - run: make deps - save_cache: key: vendor-cache-{{ .Branch }}-{{ checksum "Gopkg.lock" }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d41a8b8b9..83dd6046f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## Kolide Fleet 2.0.0 (currently preparing for release) + +The primary new addition in Fleet 2 is the new `fleetctl` CLI and file-format, which dramatically increases the flexibility and control that administrators have over their osquery deployment. The CLI and the file format are documented [in the Fleet documentation](https://github.com/kolide/fleet/blob/master/docs/cli/README.md). + +### New Features + +* New `fleetctl` CLI for managing your entire osquery workflow via CLI, API, and source controlled files! + * You can use `fleetctl` to manage osquery packs, queries, labels, and configuration. + +* In addition to the CLI, Fleet 2.0.0 introduces a new file format for articulating labels, queries, packs, options, etc. This format is designed for composability, enabling more effective sharing and re-use of intelligence. + +```yaml +apiVersion: v1 +kind: query +spec: + name: pending_updates + query: > + select value + from plist + where + path = "/Library/Preferences/ManagedInstalls.plist" and + key = "PendingUpdateCount" and + value > "0"; +``` + +* Run live osquery queries against arbitrary subsets of your infrastructure via the `fleetctl query` command. + +* Use `fleetctl setup`, `fleetctl login`, and `fleetctl logout` to manage the authentication life-cycle via the CLI. + +* Use `fleetctl get`, `fleetctl apply`, and `fleetctl delete` to manage the state of your Fleet data. + +* Manage any osquery option you want and set platform-specific overrides with the `fleetctl` CLI and file format. + +### Upgrade Plan + +* Managing osquery options via the UI has been removed in favor of the more flexible solution provided by the CLI. If you have customized your osquery options with Fleet, there is [a database migration](server/datastore/mysql/migrations/data/20171212182458_MigrateOsqueryOptions.go) which will port your existing data into the new format when you run `fleet prepare db`. To download your osquery options after migrating your database, run `fleetctl get options > options.yaml`. Further modifications to your options should occur in this file and it should be applied with `fleetctl apply -f ./options.yaml`. + ## Kolide Fleet 1.0.8 (May 3, 2018) * Osquery 3.0+ compatibility! diff --git a/Gopkg.lock b/Gopkg.lock index 930ade42d..f3aaba7b9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -28,7 +28,13 @@ branch = "master" name = "github.com/beorn7/perks" packages = ["quantile"] - revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + +[[projects]] + name = "github.com/briandowns/spinner" + packages = ["."] + revision = "48dbb65d7bd5c74ab50d53d04c949f20e3d14944" + version = "1.0" [[projects]] name = "github.com/davecgh/go-spew" @@ -39,8 +45,8 @@ [[projects]] name = "github.com/dgrijalva/jwt-go" packages = ["."] - revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29" - version = "v3.1.0" + revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" + version = "v3.2.0" [[projects]] branch = "master" @@ -54,6 +60,12 @@ revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43" version = "v1.0.0" +[[projects]] + name = "github.com/fatih/color" + packages = ["."] + revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" + version = "v1.7.0" + [[projects]] name = "github.com/fsnotify/fsnotify" packages = ["."] @@ -66,8 +78,14 @@ "internal", "redis" ] - revision = "d1ed5c67e5794de818ea85e6b522fda02623a484" - version = "v1.4.0" + revision = "a69d19351219b6dd56f274f96d85a7014a2ec34e" + version = "v1.6.0" + +[[projects]] + name = "github.com/ghodss/yaml" + packages = ["."] + revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" + version = "v1.0.0" [[projects]] name = "github.com/go-kit/kit" @@ -111,8 +129,8 @@ "ptypes/duration", "ptypes/timestamp" ] - revision = "925541529c1fa6821df4e44ce2723319eb2be768" - version = "v1.0.0" + revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" + version = "v1.1.0" [[projects]] name = "github.com/google/uuid" @@ -145,6 +163,7 @@ ".", "hcl/ast", "hcl/parser", + "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -152,13 +171,13 @@ "json/scanner", "json/token" ] - revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" [[projects]] + branch = "master" name = "github.com/igm/sockjs-go" packages = ["sockjs"] - revision = "d276e9ffe5cc5c271b81198cc77a2adf6c4482d2" - version = "v2.0.0" + revision = "c8a8c6429d10e3b6865960ad8cb43779b8a834ef" [[projects]] name = "github.com/inconshreveable/mousetrap" @@ -173,7 +192,7 @@ ".", "reflectx" ] - revision = "05cef0741ade10ca668982355b3f3f0bcf0ff0a8" + revision = "2aeb6a910c2b94f2d5eb53d9895d80e27264ec41" [[projects]] name = "github.com/jonboulle/clockwork" @@ -191,10 +210,10 @@ branch = "master" name = "github.com/kolide/kit" packages = [ - "logutil", + "env", "version" ] - revision = "d4b803f4eea66a463259243f2e95bad5124b7310" + revision = "36eb8dc4391633d1cf91b600b999c7583439f37f" [[projects]] branch = "master" @@ -204,7 +223,7 @@ "service/internal/launcherproto", "service/uuid" ] - revision = "3580ca76a81bacd009fcadaf7de73b0cdc83f830" + revision = "cb412b945cf715149437edc45bd8369273d9ab3c" [[projects]] branch = "master" @@ -214,7 +233,7 @@ "plugin/distributed", "plugin/logger" ] - revision = "77394894ef63b4ea25e1c0c4f8a8b3d1919e1aa6" + revision = "e17dfe8f44e7ac06fb2bd1cccbf64dbfbc9e5757" [[projects]] branch = "master" @@ -225,8 +244,26 @@ [[projects]] name = "github.com/magiconair/properties" packages = ["."] - revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" - version = "v1.7.4" + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" + +[[projects]] + name = "github.com/mattn/go-colorable" + packages = ["."] + revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" + version = "v0.0.9" + +[[projects]] + name = "github.com/mattn/go-isatty" + packages = ["."] + revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" + version = "v0.0.3" + +[[projects]] + name = "github.com/mattn/go-runewidth" + packages = ["."] + revision = "9e777a8366cce605130a531d2cd6363d07ad7317" + version = "v0.0.2" [[projects]] name = "github.com/matttproud/golang_protobuf_extensions" @@ -238,7 +275,13 @@ branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff" + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + +[[projects]] + branch = "master" + name = "github.com/olekukonko/tablewriter" + packages = ["."] + revision = "d4647c9c7a84d847478d890b816b7d8b62b0b279" [[projects]] branch = "master" @@ -287,7 +330,7 @@ "internal/bitbucket.org/ww/goautoneg", "model" ] - revision = "89604d197083d4781071d3c65855d24ecfb0a563" + revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c" [[projects]] branch = "master" @@ -298,13 +341,13 @@ "nfs", "xfs" ] - revision = "cb4147076ac75738c9a7d279075a253c0cc5acbd" + revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e" [[projects]] - branch = "master" name = "github.com/russellhaering/gosaml2" packages = ["types"] - revision = "319306b5ca091ee483327895d8aa29a88e37b1be" + revision = "4f381189230874b6542a39d68f3f89dfd66f969d" + version = "v0.3.1" [[projects]] name = "github.com/russellhaering/goxmldsig" @@ -321,20 +364,20 @@ ".", "mem" ] - revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" - version = "v1.0.2" + revision = "63644898a8da0bc22138abf860edaf5277b6102e" + version = "v1.1.0" [[projects]] name = "github.com/spf13/cast" packages = ["."] - revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" - version = "v1.1.0" + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" [[projects]] name = "github.com/spf13/cobra" packages = ["."] - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" + revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4" + version = "v0.0.2" [[projects]] branch = "master" @@ -345,14 +388,14 @@ [[projects]] name = "github.com/spf13/pflag" packages = ["."] - revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" - version = "v1.0.0" + revision = "583c0c0531f06d5278b7d917446061adc344b5cd" + version = "v1.0.1" [[projects]] name = "github.com/spf13/viper" packages = ["."] - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" - version = "v1.0.0" + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" [[projects]] name = "github.com/stretchr/testify" @@ -363,20 +406,28 @@ revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" +[[projects]] + name = "github.com/urfave/cli" + packages = ["."] + revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" + version = "v1.20.0" + [[projects]] branch = "master" name = "golang.org/x/crypto" packages = [ "bcrypt", - "blowfish" + "blowfish", + "ssh/terminal" ] - revision = "1875d0a70c90e57f11972aefd42276df65e895b9" + revision = "613d6eafa307c6881a737a3c35c0e312e8d3a8c5" [[projects]] branch = "master" name = "golang.org/x/net" packages = [ "context", + "http/httpguts", "http2", "http2/hpack", "idna", @@ -384,16 +435,18 @@ "lex/httplex", "trace" ] - revision = "b417086c80e91bfa321ef761574721644b8b9f61" + revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23" [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] - revision = "8f27ce8a604014414f8dfffc25cbcde83a3f2216" + packages = [ + "unix", + "windows" + ] + revision = "78d5f264b493f125018180c204871ecf58a2dce1" [[projects]] - branch = "master" name = "golang.org/x/text" packages = [ "collate", @@ -411,13 +464,14 @@ "unicode/norm", "unicode/rangetable" ] - revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" [[projects]] branch = "master" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] - revision = "4eb30f4778eed4c258ba66527a0d4f9ec8a36c45" + revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2" [[projects]] name = "google.golang.org/grpc" @@ -430,6 +484,7 @@ "connectivity", "credentials", "encoding", + "encoding/proto", "grpclb/grpc_lb_v1/messages", "grpclog", "internal", @@ -445,8 +500,8 @@ "tap", "transport" ] - revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef" - version = "v1.9.2" + revision = "d11072e7ca9811b1100b80ca0269ac831f06d024" + version = "v1.11.3" [[projects]] name = "gopkg.in/natefinch/lumberjack.v2" @@ -455,14 +510,14 @@ version = "v2.1" [[projects]] - branch = "v2" name = "gopkg.in/yaml.v2" packages = ["."] - revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "d05b65a5cbdf631128ba86884d0c5c13290312f0f943c2066ca9467e805ea32e" + inputs-digest = "814581eac74f8110241b3cfb693b245364d99ca14c58b8f2afd51f423867168d" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 57ba9d9b7..ae6092c73 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -44,7 +44,7 @@ [[constraint]] name = "github.com/igm/sockjs-go" - version = "2.0.0" + branch = "master" [[constraint]] branch = "master" diff --git a/Makefile b/Makefile index 505d6927b..1206502be 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ endif .pre-fleetctl: $(eval APP_NAME = fleetctl) -build: fleet +build: fleet fleetctl fleet: .prefix .pre-build .pre-fleet go build -i -o build/${OUTPUT} -ldflags ${KIT_VERSION} ./cmd/fleet diff --git a/README.md b/README.md index 5ef98624c..078c5c645 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,9 @@ Documentation for Fleet can be found on [GitHub](./docs/README.md). ## Using Fleet -#### The Web UI - -Information about using the Kolide web application can be found in the [Application Documentation](./docs/application/README.md). - #### The CLI -If you're interested in learning about the (under development) `fleetctl` CLI and flexible osquery deployment file format, see the [CLI Documentation](./docs/cli/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 @@ -26,6 +22,10 @@ Resources for deploying osquery to hosts, deploying the Fleet server, installing If you are interested in accessing the Fleet REST API in order to programmatically interact with your osquery installation, please see the [API Documentation](./docs/api/README.md). +#### The Web Dashboard + +Information about using the Kolide web dashboard can be found in the [Dashboard Documentation](./docs/dashboard/README.md). + ## Developing Fleet #### Development Documentation @@ -38,7 +38,7 @@ If you have any questions, please create a [GitHub Issue](https://github.com/kol #### Chat -Please join us in the #kolide channel on [osquery Slack](https://osquery-slack.herokuapp.com/). +Please join us in the #kolide channel on [Osquery Slack](https://osquery-slack.herokuapp.com/). #### Community Projects @@ -46,3 +46,9 @@ Below are some projects created by Kolide community members. Please submit a pul - [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. + +## Kolide Cloud + +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 immediately, and your first 10 hosts are free. diff --git a/cmd/fleetctl/api.go b/cmd/fleetctl/api.go new file mode 100644 index 000000000..b94f427af --- /dev/null +++ b/cmd/fleetctl/api.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + + "github.com/kolide/fleet/server/service" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func clientFromCLI(c *cli.Context) (*service.Client, error) { + if err := makeConfigIfNotExists(c.String("config")); err != nil { + return nil, errors.Wrapf(err, "error verifying that config exists at %s", c.String("config")) + } + + config, err := readConfig(c.String("config")) + if err != nil { + return nil, err + } + + cc, ok := config.Contexts[c.String("context")] + if !ok { + return nil, fmt.Errorf("context %q is not found", c.String("context")) + } + + if cc.Address == "" { + return nil, errors.New("set the Fleet API address with: fleetctl config set --address https://localhost:8080") + } + + fleet, err := service.NewClient(cc.Address, cc.TLSSkipVerify) + if err != nil { + return nil, errors.Wrap(err, "error creating Fleet API client handler") + } + + t, err := getConfigValue(c, "token") + if err != nil { + return nil, errors.Wrap(err, "error getting token from the config") + } + + if token, ok := t.(string); ok { + fleet.SetToken(token) + } else { + return nil, errors.Errorf("token config value was not a string: %+v", t) + } + + return fleet, nil +} diff --git a/cmd/fleetctl/apply.go b/cmd/fleetctl/apply.go new file mode 100644 index 000000000..d08b6bb52 --- /dev/null +++ b/cmd/fleetctl/apply.go @@ -0,0 +1,168 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/ghodss/yaml" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +type specMetadata struct { + Kind string `json:"kind"` + Version string `json:"apiVersion"` + Spec json.RawMessage `json:"spec"` +} + +type specGroup struct { + Queries []*kolide.QuerySpec + Packs []*kolide.PackSpec + Labels []*kolide.LabelSpec + Options *kolide.OptionsSpec +} + +func specGroupFromBytes(b []byte) (*specGroup, error) { + specs := &specGroup{ + Queries: []*kolide.QuerySpec{}, + Packs: []*kolide.PackSpec{}, + Labels: []*kolide.LabelSpec{}, + } + + for _, spec := range strings.Split(string(b), "---") { + if strings.TrimSpace(spec) == "" { + continue + } + + var s specMetadata + if err := yaml.Unmarshal([]byte(spec), &s); err != nil { + return nil, err + } + + if s.Spec == nil { + return nil, errors.Errorf("no spec field on %q document", s.Kind) + } + + switch strings.ToLower(s.Kind) { + case "query": + var querySpec *kolide.QuerySpec + if err := yaml.Unmarshal(s.Spec, &querySpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling query spec") + } + specs.Queries = append(specs.Queries, querySpec) + + case "pack": + var packSpec *kolide.PackSpec + if err := yaml.Unmarshal(s.Spec, &packSpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling pack spec") + } + specs.Packs = append(specs.Packs, packSpec) + + case "label": + var labelSpec *kolide.LabelSpec + if err := yaml.Unmarshal(s.Spec, &labelSpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling label spec") + } + specs.Labels = append(specs.Labels, labelSpec) + + case "options": + if specs.Options != nil { + return nil, errors.New("options defined twice in the same file") + } + + var optionSpec *kolide.OptionsSpec + if err := yaml.Unmarshal(s.Spec, &optionSpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling option spec") + } + specs.Options = optionSpec + + default: + return nil, errors.Errorf("unknown kind %q", s.Kind) + } + } + + return specs, nil +} + +func applyCommand() cli.Command { + var ( + flFilename string + flDebug bool + ) + return cli.Command{ + Name: "apply", + Usage: "Apply files to declaratively manage osquery configurations", + UsageText: `fleetctl apply [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "f", + EnvVar: "FILENAME", + Value: "", + Destination: &flFilename, + Usage: "A file to apply", + }, + cli.BoolFlag{ + Name: "debug", + EnvVar: "DEBUG", + Destination: &flDebug, + Usage: "Whether or not to enable debug logging", + }, + }, + Action: func(c *cli.Context) error { + if flFilename == "" { + return errors.New("-f must be specified") + } + + b, err := ioutil.ReadFile(flFilename) + if err != nil { + return err + } + + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + specs, err := specGroupFromBytes(b) + if err != nil { + return err + } + + if len(specs.Queries) > 0 { + if err := fleet.ApplyQueries(specs.Queries); err != nil { + return errors.Wrap(err, "applying queries") + } + fmt.Printf("[+] applied %d queries\n", len(specs.Queries)) + } + + if len(specs.Labels) > 0 { + if err := fleet.ApplyLabels(specs.Labels); err != nil { + return errors.Wrap(err, "applying labels") + } + fmt.Printf("[+] applied %d labels\n", len(specs.Labels)) + } + + if len(specs.Packs) > 0 { + if err := fleet.ApplyPacks(specs.Packs); err != nil { + return errors.Wrap(err, "applying packs") + } + fmt.Printf("[+] applied %d packs\n", len(specs.Packs)) + } + + if specs.Options != nil { + if err := fleet.ApplyOptions(specs.Options); err != nil { + return errors.Wrap(err, "applying options") + } + fmt.Printf("[+] applied options\n") + + } + + return nil + }, + } +} diff --git a/cmd/fleetctl/config.go b/cmd/fleetctl/config.go new file mode 100644 index 000000000..27ce9a16e --- /dev/null +++ b/cmd/fleetctl/config.go @@ -0,0 +1,296 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + + "github.com/ghodss/yaml" + "github.com/kolide/kit/env" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +const ( + configFilePerms = 0600 +) + +type configFile struct { + Contexts map[string]Context `json:"contexts"` +} + +type Context struct { + Address string `json:"address"` + Email string `json:"email"` + Token string `json:"token"` + TLSSkipVerify bool `json:"tls-skip-verify"` +} + +func configFlag() cli.Flag { + return cli.StringFlag{ + Name: "config", + Value: fmt.Sprintf("%s/.fleet/config", env.String("HOME", "~/")), + EnvVar: "CONFIG", + Usage: "Path to the Fleet config file", + } +} + +func contextFlag() cli.Flag { + return cli.StringFlag{ + Name: "context", + Value: "default", + EnvVar: "CONTEXT", + Usage: "Name of Fleet config context to use", + } +} + +func makeConfigIfNotExists(fp string) error { + if _, err := os.Stat(filepath.Dir(fp)); os.IsNotExist(err) { + if err := os.Mkdir(filepath.Dir(fp), 0700); err != nil { + return err + } + } + + _, err := os.OpenFile(fp, os.O_RDONLY|os.O_CREATE, configFilePerms) + return err +} + +func readConfig(fp string) (c configFile, err error) { + b, err := ioutil.ReadFile(fp) + if err != nil { + return + } + + err = yaml.Unmarshal(b, &c) + + if c.Contexts == nil { + c.Contexts = map[string]Context{ + "default": Context{}, + } + } + return +} + +func writeConfig(fp string, c configFile) error { + b, err := yaml.Marshal(c) + if err != nil { + return err + } + + return ioutil.WriteFile(fp, b, configFilePerms) +} + +func getConfigValue(c *cli.Context, key string) (interface{}, error) { + var ( + flContext string + flConfig string + ) + + flConfig = c.String("config") + flContext = c.String("context") + + if err := makeConfigIfNotExists(flConfig); err != nil { + return nil, errors.Wrapf(err, "error verifying that config exists at %s", flConfig) + } + + config, err := readConfig(flConfig) + if err != nil { + return nil, errors.Wrapf(err, "error reading config at %s", flConfig) + } + + currentContext, ok := config.Contexts[flContext] + if !ok { + fmt.Printf("[+] Context %q not found, creating it with default values\n", flContext) + currentContext = Context{} + } + + switch key { + case "address": + return currentContext.Address, nil + case "email": + return currentContext.Email, nil + case "token": + return currentContext.Token, nil + case "tls-skip-verify": + if currentContext.TLSSkipVerify { + return true, nil + } else { + return false, nil + } + default: + return nil, fmt.Errorf("%q is an invalid key", key) + } +} + +func setConfigValue(c *cli.Context, key, value string) error { + var ( + flContext string + flConfig string + ) + + flConfig = c.String("config") + flContext = c.String("context") + + if err := makeConfigIfNotExists(flConfig); err != nil { + return errors.Wrapf(err, "error verifying that config exists at %s", flConfig) + } + + config, err := readConfig(flConfig) + if err != nil { + return errors.Wrapf(err, "error reading config at %s", flConfig) + } + + currentContext, ok := config.Contexts[flContext] + if !ok { + fmt.Printf("[+] Context %q not found, creating it with default values\n", flContext) + currentContext = Context{} + } + + switch key { + case "address": + currentContext.Address = value + case "email": + currentContext.Email = value + case "token": + currentContext.Token = value + case "tls-skip-verify": + boolValue, err := strconv.ParseBool(value) + if err != nil { + return errors.Wrapf(err, "error parsing %q as bool", value) + } + currentContext.TLSSkipVerify = boolValue + default: + return fmt.Errorf("%q is an invalid option", key) + } + + config.Contexts[flContext] = currentContext + + if err := writeConfig(flConfig, config); err != nil { + return errors.Wrap(err, "error saving config file") + } + + return nil +} + +func configSetCommand() cli.Command { + var ( + flAddress string + flEmail string + flToken string + flTLSSkipVerify bool + ) + return cli.Command{ + Name: "set", + Usage: "Set config options", + UsageText: `fleetctl config set [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "address", + EnvVar: "ADDRESS", + Value: "", + Destination: &flAddress, + Usage: "Address of the Fleet server", + }, + cli.StringFlag{ + Name: "email", + EnvVar: "EMAIL", + Value: "", + Destination: &flEmail, + Usage: "Email to use when connecting to the Fleet server", + }, + cli.StringFlag{ + Name: "token", + EnvVar: "TOKEN", + Value: "", + Destination: &flToken, + Usage: "Fleet API token", + }, + cli.BoolFlag{ + Name: "tls-skip-verify", + EnvVar: "INSECURE", + Destination: &flTLSSkipVerify, + Usage: "Skip TLS certificate validation", + }, + }, + Action: func(c *cli.Context) error { + set := false + + if flAddress != "" { + set = true + if err := setConfigValue(c, "address", flAddress); err != nil { + return errors.Wrap(err, "error setting address") + } + fmt.Printf("[+] Set the address config key to %q in the %q context\n", flAddress, c.String("context")) + } + + if flEmail != "" { + set = true + if err := setConfigValue(c, "email", flEmail); err != nil { + return errors.Wrap(err, "error setting email") + } + fmt.Printf("[+] Set the email config key to %q in the %q context\n", flEmail, c.String("context")) + } + + if flToken != "" { + set = true + if err := setConfigValue(c, "token", flToken); err != nil { + return errors.Wrap(err, "error setting token") + } + fmt.Printf("[+] Set the token config key to %q in the %q context\n", flToken, c.String("context")) + } + + if flTLSSkipVerify { + set = true + if err := setConfigValue(c, "tls-skip-verify", "true"); err != nil { + return errors.Wrap(err, "error setting tls-skip-verify") + } + fmt.Printf("[+] Set the tls-skip-verify config key to \"true\" in the %q context\n", c.String("context")) + } + + if !set { + return cli.ShowCommandHelp(c, "set") + } + + return nil + }, + } +} + +func configGetCommand() cli.Command { + return cli.Command{ + Name: "get", + Usage: "Get a config option", + UsageText: `fleetctl config get [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + if len(c.Args()) != 1 { + return cli.ShowCommandHelp(c, "get") + } + + key := c.Args()[0] + + // validate key + switch key { + case "address", "email", "token", "tls-skip-verify": + default: + return cli.ShowCommandHelp(c, "get") + } + + value, err := getConfigValue(c, key) + if err != nil { + return errors.Wrap(err, "error getting config value") + } + + fmt.Printf(" %s.%s => %s\n", c.String("context"), key, value) + + return nil + }, + } +} diff --git a/cmd/fleetctl/convert.go b/cmd/fleetctl/convert.go new file mode 100644 index 000000000..11c9671e9 --- /dev/null +++ b/cmd/fleetctl/convert.go @@ -0,0 +1,165 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strconv" + "strings" + + "github.com/ghodss/yaml" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func specGroupFromPack(name string, inputPack kolide.PermissivePackContent) (*specGroup, error) { + specs := &specGroup{ + Queries: []*kolide.QuerySpec{}, + Packs: []*kolide.PackSpec{}, + Labels: []*kolide.LabelSpec{}, + } + + pack := &kolide.PackSpec{ + Name: name, + } + + for name, query := range inputPack.Queries { + spec := &kolide.QuerySpec{ + Name: name, + Description: query.Description, + Query: query.Query, + } + + interval := uint(0) + switch i := query.Interval.(type) { + case string: + u64, err := strconv.ParseUint(i, 10, 32) + if err != nil { + return nil, errors.Wrap(err, "converting interval from string to uint") + } + interval = uint(u64) + case uint: + interval = i + } + + specs.Queries = append(specs.Queries, spec) + pack.Queries = append(pack.Queries, kolide.PackSpecQuery{ + Name: name, + QueryName: name, + Interval: interval, + Description: query.Description, + Snapshot: query.Snapshot, + Removed: query.Removed, + Shard: query.Shard, + Platform: query.Platform, + Version: query.Version, + }) + } + + specs.Packs = append(specs.Packs, pack) + + return specs, nil +} + +func convertCommand() cli.Command { + var ( + flFilename string + flDebug bool + ) + return cli.Command{ + Name: "convert", + Usage: "Convert osquery packs into decomposed fleet configs", + UsageText: `fleetctl convert [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "f", + EnvVar: "FILENAME", + Value: "", + Destination: &flFilename, + Usage: "A file to apply", + }, + cli.BoolFlag{ + Name: "debug", + EnvVar: "DEBUG", + Destination: &flDebug, + Usage: "Whether or not to enable debug logging", + }, + }, + Action: func(c *cli.Context) error { + if flFilename == "" { + return errors.New("-f must be specified") + } + + b, err := ioutil.ReadFile(flFilename) + if err != nil { + return err + } + + var specs *specGroup + + var pack kolide.PermissivePackContent + packErr := json.Unmarshal(b, &pack) + if packErr == nil { + base := filepath.Base(flFilename) + specs, err = specGroupFromPack(strings.TrimSuffix(base, filepath.Ext(base)), pack) + if err != nil { + return err + } + } else { + return packErr + } + + if specs == nil { + return errors.New("could not parse files") + } + + for _, pack := range specs.Packs { + spec, err := json.Marshal(pack) + if err != nil { + return err + } + + meta := specMetadata{ + Kind: "pack", + Version: "v1", + Spec: spec, + } + + out, err := yaml.Marshal(meta) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Print(string(out)) + } + + for _, query := range specs.Queries { + spec, err := json.Marshal(query) + if err != nil { + return err + } + + meta := specMetadata{ + Kind: "query", + Version: "v1", + Spec: spec, + } + + out, err := yaml.Marshal(meta) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Print(string(out)) + } + + return nil + }, + } +} diff --git a/cmd/fleetctl/delete.go b/cmd/fleetctl/delete.go new file mode 100644 index 000000000..aa9c1ac21 --- /dev/null +++ b/cmd/fleetctl/delete.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/kolide/fleet/server/service" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func deleteCommand() cli.Command { + var ( + flFilename string + flDebug bool + ) + return cli.Command{ + Name: "delete", + Usage: "Specify files to declaratively batch delete osquery configurations", + UsageText: `fleetctl delete [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "f", + EnvVar: "FILENAME", + Value: "", + Destination: &flFilename, + Usage: "A file to apply", + }, + cli.BoolFlag{ + Name: "debug", + EnvVar: "DEBUG", + Destination: &flDebug, + Usage: "Whether or not to enable debug logging", + }, + }, + Action: func(c *cli.Context) error { + if flFilename == "" { + return errors.New("-f must be specified") + } + + b, err := ioutil.ReadFile(flFilename) + if err != nil { + return err + } + + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + specs, err := specGroupFromBytes(b) + if err != nil { + return err + } + + for _, query := range specs.Queries { + fmt.Printf("[+] deleting query %q\n", query.Name) + if err := fleet.DeleteQuery(query.Name); err != nil { + switch err.(type) { + case service.NotFoundErr: + fmt.Printf("[!] query %q doesn't exist\n", query.Name) + continue + } + return err + } + } + + for _, pack := range specs.Packs { + fmt.Printf("[+] deleting pack %q\n", pack.Name) + if err := fleet.DeletePack(pack.Name); err != nil { + switch err.(type) { + case service.NotFoundErr: + fmt.Printf("[!] pack %q doesn't exist\n", pack.Name) + continue + } + return err + } + } + + for _, label := range specs.Labels { + fmt.Printf("[+] deleting label %q\n", label.Name) + if err := fleet.DeleteLabel(label.Name); err != nil { + switch err.(type) { + case service.NotFoundErr: + fmt.Printf("[!] label %q doesn't exist\n", label.Name) + continue + } + return err + } + } + + return nil + }, + } +} diff --git a/cmd/fleetctl/fleetctl.go b/cmd/fleetctl/fleetctl.go index f7ecc0090..859e29d19 100644 --- a/cmd/fleetctl/fleetctl.go +++ b/cmd/fleetctl/fleetctl.go @@ -1,83 +1,53 @@ package main import ( - "fmt" - "os" - "strings" + "math/rand" + "time" - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/kit/logutil" "github.com/kolide/kit/version" + "github.com/urfave/cli" ) -func runVersion(args []string) error { - version.PrintFull() - return nil -} - -func runNoop(args []string) error { - fmt.Printf("%+v\n", args) - return nil -} - -type runFunc func([]string) error -type subcommandMap map[string]runFunc -type commandMap map[string]subcommandMap - -func usage() { - fmt.Fprintf(os.Stderr, "fleetctl controls an instance of the Kolide Fleet osquery fleet manager.\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, "Find more information at https://kolide.com/fleet\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, " Usage:\n") - fmt.Fprintf(os.Stderr, " fleetctl [command] [flags]\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, " Commands:\n") - fmt.Fprintf(os.Stderr, " fleetctl query - run a query across your fleet\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, " fleetctl apply - apply a set of osquery configurations\n") - fmt.Fprintf(os.Stderr, " fleetctl edit - edit your complete configuration in an ephemeral editor\n") - fmt.Fprintf(os.Stderr, " fleetctl config - modify how and which Fleet server to connect to\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, " fleetctl help - get help on how to define an intent type\n") - fmt.Fprintf(os.Stderr, " fleetctl version - print full version information\n") - fmt.Fprintf(os.Stderr, "\n") +func init() { + rand.Seed(time.Now().UnixNano()) } func main() { - logger := level.NewFilter(log.NewJSONLogger(os.Stderr), level.AllowDebug()) - logger = log.With(logger, "ts", log.DefaultTimestampUTC) - logger = log.With(logger, "caller", log.DefaultCaller) - - if len(os.Args) < 2 { - usage() - os.Exit(0) + app := cli.NewApp() + app.Name = "fleetctl" + app.Usage = "CLI for operating Kolide Fleet" + app.Version = version.Version().Version + cli.VersionPrinter = func(c *cli.Context) { + version.PrintFull() } - var run func([]string) error - switch strings.ToLower(os.Args[1]) { - case "version": - run = runVersion - case "query": - run = runNoop - case "edit": - run = runNoop - case "new": - run = runNoop - case "apply": - run = runNoop - case "config": - run = runNoop - case "help": - run = runNoop - default: - usage() - os.Exit(1) + app.Commands = []cli.Command{ + applyCommand(), + deleteCommand(), + setupCommand(), + loginCommand(), + logoutCommand(), + queryCommand(), + cli.Command{ + Name: "get", + Usage: "Get/list resources", + Subcommands: []cli.Command{ + getQueriesCommand(), + getPacksCommand(), + getLabelsCommand(), + getOptionsCommand(), + }, + }, + cli.Command{ + Name: "config", + Usage: "Modify how and which Fleet server to connect to", + Subcommands: []cli.Command{ + configSetCommand(), + configGetCommand(), + }, + }, + convertCommand(), } - if err := run(os.Args[2:]); err != nil { - logutil.Fatal(logger, "err", err) - } + app.RunAndExitOnError() } diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go new file mode 100644 index 000000000..c7a971484 --- /dev/null +++ b/cmd/fleetctl/get.go @@ -0,0 +1,270 @@ +package main + +import ( + "fmt" + "os" + + "github.com/ghodss/yaml" + "github.com/kolide/fleet/server/kolide" + "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +type specGeneric struct { + Kind string `json:"kind"` + Version string `json:"apiVersion"` + Spec interface{} `json:"spec"` +} + +func defaultTable() *tablewriter.Table { + table := tablewriter.NewWriter(os.Stdout) + table.SetRowLine(true) + return table +} + +func getQueriesCommand() cli.Command { + return cli.Command{ + Name: "queries", + Aliases: []string{"query", "q"}, + Usage: "List information about one or more queries", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + name := c.Args().First() + + // if name wasn't provided, list all queries + if name == "" { + queries, err := fleet.GetQueries() + if err != nil { + return errors.Wrap(err, "could not list queries") + } + + if len(queries) == 0 { + fmt.Println("no queries found") + return nil + } + + data := [][]string{} + + for _, query := range queries { + data = append(data, []string{ + query.Name, + query.Description, + query.Query, + }) + } + + table := defaultTable() + table.SetHeader([]string{"name", "description", "query"}) + table.AppendBulk(data) + table.Render() + + return nil + } else { + query, err := fleet.GetQuery(name) + if err != nil { + return err + } + + spec := specGeneric{ + Kind: "query", + Version: kolide.ApiVersion, + Spec: query, + } + + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + + fmt.Print(string(b)) + return nil + } + }, + } +} + +func getPacksCommand() cli.Command { + return cli.Command{ + Name: "packs", + Aliases: []string{"pack", "p"}, + Usage: "List information about one or more packs", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + name := c.Args().First() + + // if name wasn't provided, list all packs + if name == "" { + packs, err := fleet.GetPacks() + if err != nil { + return errors.Wrap(err, "could not list packs") + } + + if len(packs) == 0 { + fmt.Println("no packs found") + return nil + } + + data := [][]string{} + + for _, pack := range packs { + data = append(data, []string{ + pack.Name, + pack.Platform, + pack.Description, + }) + } + + table := defaultTable() + table.SetHeader([]string{"name", "platform", "description"}) + table.AppendBulk(data) + table.Render() + + return nil + } else { + pack, err := fleet.GetPack(name) + if err != nil { + return err + } + + spec := specGeneric{ + Kind: "pack", + Version: kolide.ApiVersion, + Spec: pack, + } + + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + + fmt.Print(string(b)) + return nil + } + }, + } +} + +func getLabelsCommand() cli.Command { + return cli.Command{ + Name: "labels", + Aliases: []string{"label", "l"}, + Usage: "List information about one or more labels", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + name := c.Args().First() + + // if name wasn't provided, list all labels + if name == "" { + labels, err := fleet.GetLabels() + if err != nil { + return errors.Wrap(err, "could not list labels") + } + + if len(labels) == 0 { + fmt.Println("no labels found") + return nil + } + + data := [][]string{} + + for _, label := range labels { + data = append(data, []string{ + label.Name, + label.Platform, + label.Description, + label.Query, + }) + } + + table := defaultTable() + table.SetHeader([]string{"name", "platform", "description", "query"}) + table.AppendBulk(data) + table.Render() + + return nil + } else { + label, err := fleet.GetLabel(name) + if err != nil { + return err + } + + spec := specGeneric{ + Kind: "label", + Version: kolide.ApiVersion, + Spec: label, + } + + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + + fmt.Print(string(b)) + + return nil + } + }, + } +} + +func getOptionsCommand() cli.Command { + return cli.Command{ + Name: "options", + Usage: "Retrieve the osquery configuration", + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + options, err := fleet.GetOptions() + if err != nil { + return err + } + + spec := specGeneric{ + Kind: "options", + Version: kolide.ApiVersion, + Spec: options, + } + + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + + fmt.Print(string(b)) + return nil + + }, + } +} diff --git a/cmd/fleetctl/login.go b/cmd/fleetctl/login.go new file mode 100644 index 000000000..fe694aad8 --- /dev/null +++ b/cmd/fleetctl/login.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "os" + + "github.com/kolide/fleet/server/service" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" +) + +func loginCommand() cli.Command { + var ( + flEmail string + flPassword string + ) + return cli.Command{ + Name: "login", + Usage: "Login to Kolide Fleet", + UsageText: ` +fleetctl login [options] + +Interactively prompts for email and password if not specified in the flags or environment variables. +`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "email", + EnvVar: "EMAIL", + Value: "", + Destination: &flEmail, + Usage: "Email to use to log in", + }, + cli.StringFlag{ + Name: "password", + EnvVar: "PASSWORD", + Value: "", + Destination: &flPassword, + Usage: "Password to use to log in (recommended to use interactive entry)", + }, + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + // Allow interactive entry to discourage passwords in + // CLI history. + if flEmail == "" { + fmt.Println("Log in using the standard Fleet credentials.") + fmt.Print("Email: ") + _, err := fmt.Scanln(&flEmail) + if err != nil { + return errors.Wrap(err, "error reading email") + } + } + if flPassword == "" { + fmt.Print("Password: ") + passBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return errors.Wrap(err, "error reading password") + } + flPassword = string(passBytes) + } + + token, err := fleet.Login(flEmail, flPassword) + if err != nil { + switch err.(type) { + case service.InvalidLoginErr: + return err + case service.NotSetupErr: + return err + } + return errors.Wrap(err, "error logging in") + } + + if err := setConfigValue(c, "email", flEmail); err != nil { + return errors.Wrap(err, "error setting email for the current context") + } + + if err := setConfigValue(c, "token", token); err != nil { + return errors.Wrap(err, "error setting token for the current context") + } + + fmt.Printf("[+] Fleet login successful and context configured!\n") + + return nil + }, + } +} diff --git a/cmd/fleetctl/logout.go b/cmd/fleetctl/logout.go new file mode 100644 index 000000000..211d4bd92 --- /dev/null +++ b/cmd/fleetctl/logout.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func logoutCommand() cli.Command { + return cli.Command{ + Name: "logout", + Usage: "Logout of Kolide Fleet", + UsageText: `fleetctl logout [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + if err := fleet.Logout(); err != nil { + return errors.Wrap(err, "error logging in") + } + + if err := setConfigValue(c, "token", ""); err != nil { + return errors.Wrap(err, "error setting token for the current context") + } + + fmt.Printf("[+] Fleet logout successful and local token cleared!\n") + + return nil + }, + } +} diff --git a/cmd/fleetctl/query.go b/cmd/fleetctl/query.go new file mode 100644 index 000000000..de7286d77 --- /dev/null +++ b/cmd/fleetctl/query.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + "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 ( + flFilename, flHosts, flLabels, flQuery string + flDebug bool + ) + return cli.Command{ + Name: "query", + Usage: "Run a live query", + UsageText: `fleetctl query [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "f", + EnvVar: "FILENAME", + Value: "", + Destination: &flFilename, + Usage: "A file to apply", + }, + cli.StringFlag{ + Name: "hosts", + EnvVar: "HOSTS", + Value: "", + Destination: &flHosts, + Usage: "Comma separated hostnames to target", + }, + cli.StringFlag{ + Name: "labels", + EnvVar: "LABELS", + Value: "", + Destination: &flLabels, + Usage: "Comma separated label names to target", + }, + cli.StringFlag{ + Name: "query", + EnvVar: "QUERY", + Value: "", + Destination: &flQuery, + Usage: "Query to run", + }, + cli.BoolFlag{ + Name: "debug", + EnvVar: "DEBUG", + Destination: &flDebug, + Usage: "Whether or not to enable debug logging", + }, + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + if flHosts == "" && flLabels == "" { + return errors.New("No hosts or labels targeted") + } + + if flQuery == "" { + return errors.New("No query specified") + } + + hosts := strings.Split(flHosts, ",") + labels := strings.Split(flLabels, ",") + + res, err := fleet.LiveQuery(flQuery, labels, hosts) + if err != nil { + return err + } + + tick := time.NewTicker(100 * time.Millisecond) + defer tick.Stop() + + // See charsets at + // https://godoc.org/github.com/briandowns/spinner#pkg-variables + s := spinner.New(spinner.CharSets[24], 200*time.Millisecond) + s.Writer = os.Stderr + s.Start() + + for { + select { + case hostResult := <-res.Results(): + out := resultOutput{hostResult.Host.HostName, hostResult.Rows} + if err := json.NewEncoder(os.Stdout).Encode(out); err != nil { + fmt.Fprintf(os.Stderr, "Error writing output: %s\n", err) + } + + case err := <-res.Errors(): + fmt.Fprintf(os.Stderr, "Error talking to server: %s\n", err.Error()) + + case <-tick.C: + // Print status message to stderr + status := res.Status() + totals := res.Totals() + var percentTotal, percentOnline float64 + var responded, total, online uint + if status != nil && totals != nil { + total = totals.Total + online = totals.Online + responded = status.ActualResults + if total > 0 { + percentTotal = 100 * float64(responded) / float64(total) + } + if online > 0 { + percentOnline = 100 * float64(responded) / float64(online) + } + } + s.Suffix = fmt.Sprintf(" %.f%% responded (%.f%% online) | %d/%d targeted hosts (%d/%d online)", percentTotal, percentOnline, responded, total, responded, online) + } + } + }, + } +} diff --git a/cmd/fleetctl/setup.go b/cmd/fleetctl/setup.go new file mode 100644 index 000000000..4ccd67080 --- /dev/null +++ b/cmd/fleetctl/setup.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + + "github.com/kolide/fleet/server/service" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func setupCommand() cli.Command { + var ( + flEmail string + flPassword string + flOrgName string + ) + return cli.Command{ + Name: "setup", + Usage: "Setup a Kolide Fleet instance", + UsageText: `fleetctl config login [options]`, + Flags: []cli.Flag{ + configFlag(), + contextFlag(), + cli.StringFlag{ + Name: "email", + EnvVar: "EMAIL", + Value: "", + Destination: &flEmail, + Usage: "Email of the admin user to create", + }, + cli.StringFlag{ + Name: "password", + EnvVar: "PASSWORD", + Value: "", + Destination: &flPassword, + Usage: "Password for the admin user", + }, + cli.StringFlag{ + Name: "org-name", + EnvVar: "ORG_NAME", + Value: "", + Destination: &flOrgName, + Usage: "Name of the organization", + }, + }, + Action: func(c *cli.Context) error { + fleet, err := clientFromCLI(c) + if err != nil { + return err + } + + if flEmail == "" { + return errors.Errorf("Email of the admin user to create must be provided") + } + if flPassword == "" { + return errors.Errorf("Password for the admin user to create must be provided") + } + token, err := fleet.Setup(flEmail, flPassword, flOrgName) + if err != nil { + switch err.(type) { + case service.SetupAlreadyErr: + return err + } + return errors.Wrap(err, "error setting up Fleet") + } + + if err := setConfigValue(c, "email", flEmail); err != nil { + return errors.Wrap(err, "error setting email for the current context") + } + + if err := setConfigValue(c, "token", token); err != nil { + return errors.Wrap(err, "error setting token for the current context") + } + + fmt.Printf("[+] Fleet setup successful and context configured!\n") + + return nil + }, + } +} diff --git a/docs/README.md b/docs/README.md index b23944532..bf6c1e130 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,31 +1,11 @@ -Kolide Documentation -==================== +# Fleet Documentation -Welcome to the Kolide documentation. +Welcome to the documentation for the Kolide Fleet osquery fleet manager. -- Information about using the Kolide web application can be found in the [Application Documentation](./application/README.md). -- If you're interested in using the new `fleetctl` CLI to manage your osquery fleet, see the [CLI Documentation](./cli/README.md). +- If you're interested in using the `fleetctl` CLI to manage your osquery fleet, see the [CLI Documentation](./cli/README.md). - Resources for deploying osquery to hosts, deploying the Kolide server, installing Kolide's infrastructure dependencies, etc. can all be found in the [Infrastructure Documentation](./infrastructure/README.md). - If you are interested in accessing the Kolide REST API in order to programmatically interact with your osquery installation, please see the [API Documentation](./api/README.md). +- Information about using the Kolide web dashboard can be found in the [Dashboard Documentation](./dashboard/README.md). - Finally, 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](./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.herokuapp.com/). You can find us in the `#kolide` channel. - -# Troubleshooting FAQ - -## Make errors - -``` -/bin/bash: dep: command not found -make: *** [.deps] Error 127 -``` - -If you get the above error, you need to add `$GOPATH/bin` to your PATH. A quick fix is to run `export PATH=$GOPATH/bin:$PATH`. -See the Go language documentation for [workspaces](https://golang.org/doc/code.html#Workspaces) and [GOPATH](https://golang.org/doc/code.html#GOPATH) for a more indepth documentation. - -``` -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. diff --git a/docs/application/README.md b/docs/application/README.md deleted file mode 100644 index 83f59207e..000000000 --- a/docs/application/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Application Documentation -========================= - -Kolide Fleet is an application that allows you to take advantage of the power of osquery in order to maintain constant insight into the state of your infrastructure (security, health, stability, performance, compliance, etc). The application documentation contains documents on the following topics: - -## Using the Kolide Fleet application - -- For information on running osquery queries on hosts in your infrastructure, you can refer to the [Running Queries](./running-queries.md) page. -- To learn more about scheduling queries for periodic execution on select hosts, managing query packs, etc, you can refer to the [Scheduling Queries](./scheduling-queries.md) page. -- Fleet also allows you to configure osquery options so that you can endlessly customize your osquery usage. For information on how to customize osquery using Fleet as well as thoughts on what customization you might consider performing, see the [Configuring Osquery Options](./configuring-osquery-options.md) documentation. - -## Working with osquery logs - -Fleet makes it easy 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. diff --git a/docs/application/configuring-osquery-options.md b/docs/application/configuring-osquery-options.md deleted file mode 100644 index 814d01ece..000000000 --- a/docs/application/configuring-osquery-options.md +++ /dev/null @@ -1,8 +0,0 @@ -Configuring Osquery Options -=========================== - -To connect a host to Fleet, you have to launch `osqueryd` with some very specific options (which are further outlined in the [Adding Hosts To Fleet](../infrastructure/adding-hosts-to-fleet.md) documentation). Once a host has connected, many global osquery configuration options can be changed without necessitating a reboot of osquery. These options can be edited and managed by selecting the "Config" sidebar. - -![Manage Osquery Options](../images/manage-osquery-options.png) - -Note that this is a more advanced feature. The Fleet application will not allow you to configure osquery such that it can't talk to Fleet anymore, but you can significantly modify the behavior of osquery via configuration. diff --git a/docs/application/scheduling-queries.md b/docs/application/scheduling-queries.md deleted file mode 100644 index 0a87bf5b3..000000000 --- a/docs/application/scheduling-queries.md +++ /dev/null @@ -1,28 +0,0 @@ -Scheduling Queries -================== - -As discussed in the [Running Queries Documentation](./running-queries.md), you can use the Fleet application to create, execute, and save osquery queries. You can organize these queries into "Query Packs". To view all saved packs and perhaps create a new pack, select "Manage Packs" from the "Packs" sidebar. Packs are usually organized by the general class of instrumentation that you're trying to perform. - -![Manage Packs](../images/manage-packs.png) - -If you select a pack from the list, you can quickly enable and disable the entire pack, or you can configure it further. - -![Manage Packs With Pack Selected](../images/manage-packs-with-pack-selected.png) - -When you edit a pack, you can decide which targets you would like to execute the pack. This is a similar selection experience to the target selection process that you use to execute a new query. - -![Edit Pack Targets](../images/edit-pack-targets.png) - -To add queries to a pack, use the right-hand sidebar. You can take an existing scheduled query and add it to the pack. You must also define a few key details such as: - -- interval: how often should the query be executed? -- logging: which osquery logging format would you like to use? -- platform: which operating system platforms should execute this query? -- minimum osquery version: if the table was introduced in a newer version of osquery, you may want to ensure that only sufficiently recent version of osquery execute the query. -- shard: from 0 to 100, what percent of hosts should execute this query? - -![Schedule Query Sidebar](../images/schedule-query-sidebar.png) - - -Once you've scheduled queries and curated your packs, you can read our guide to [Working With Osquery Logs](./working-with-osquery-logs.md). - diff --git a/docs/cli/README.md b/docs/cli/README.md index a3ec43c44..28fbef9e6 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -1,11 +1,7 @@ CLI Documentation ================= -Kolide Fleet provides a server which allows you to manage and orchestrate an osquery deployment across of a set of workstations and servers. For certain use-cases, it makes sense to maintain the configuration and data of an osquery deployment in source-controlled files. It is also desirable to be able to manage these files with a familiar command-line tool. To facilitate this, we are working on an experimental CLI called `fleetctl`. - -### Warning: In Progress - -This CLI is largely just a proposal and large sections (if not most) of this do not work. The objective user-experience is documented here so that contributors working on this feature can share documentation with the community to gather feedback. +Kolide Fleet provides a server which allows you to manage and orchestrate an osquery deployment across of a set of workstations and servers. For certain use-cases, it makes sense to maintain the configuration and data of an osquery deployment in source-controlled files. It is also desirable to be able to manage these files with a familiar command-line tool. To facilitate this, Kolide is working on an experimental CLI called `fleetctl`. ## Inspiration @@ -27,24 +23,29 @@ Similarly, Fleet objects can be created, updated, and deleted by storing multipl ### Help Output ``` -$ fleetctl --help -fleetctl controls an instance of the Kolide Fleet osquery fleet manager. +NAME: + fleetctl - The CLI for operating Kolide Fleet -Find more information at https://kolide.com/fleet +USAGE: + fleetctl [global options] command [command options] [arguments...] - Usage: - fleetctl [command] [flags] +VERSION: + 2.0.0-rc1 +COMMANDS: + query Run an osquery distributed query + apply Apply files to declaratively manage osquery configurations + delete Specify files to delete + setup Setup a Kolide Fleet instance + login Login to Kolide Fleet + logout Logout of Kolide Fleet + get Get/list resources + config Modify how and which Fleet server to connect to + help, h Shows a list of commands or help for one command - Commands: - fleetctl query - run a query across your fleet - - fleetctl apply - apply a set of osquery configurations - fleetctl edit - edit your complete configuration in an ephemeral editor - fleetctl config - modify how and which Fleet server to connect to - - fleetctl help - get help on how to define an intent type - fleetctl version - print full version information +GLOBAL OPTIONS: + --help, -h show help + --version, -v print the version ``` ### Workflow @@ -95,8 +96,8 @@ All of these files can be concatenated together into [one file](../../examples/c The following file describes configuration options passed to the osquery instance. All other configuration data will be over-written by the application of this file. ```yaml -apiVersion: kolide.com/v1alpha1 -kind: OsqueryOptions +apiVersion: v1 +kind: options spec: config: options: @@ -170,14 +171,14 @@ spec: The following file describes the labels which hosts should be automatically grouped into. The label resource should reference the query by name. Both of these resources can be included in the same file as such: ```yaml -apiVersion: kolide.com/v1alpha1 -kind: OsqueryLabel +apiVersion: v1 +kind: label spec: name: slack_not_running query: slack_not_running --- apiVersion: kolide.com/v1/alpha1 -kind: OsqueryQuery +kind: query spec: name: slack_not_running query: > @@ -194,8 +195,8 @@ spec: For especially long or complex queries, you may want to define one query in one file. Continued edits and applications to this file will update the query as long as the `metadata.name` does not change. If you want to change the name of a query, you must first create a new query with the new name and then delete the query with the old name. Make sure the old query name is not defined in any packs before deleting it or an error will occur. ```yaml -apiVersion: kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: docker_processes descriptions: The docker containers processes that are running on a system. @@ -207,11 +208,11 @@ spec: - darwin ``` -To define multiple queries in a file, concatenate multiple `OsqueryQuery` resources together in a single file with `---`. For example, consider a file that you might store at `queries/osquery_monitoring.yml`: +To define multiple queries in a file, concatenate multiple `query` resources together in a single file with `---`. For example, consider a file that you might store at `queries/osquery_monitoring.yml`: ```yaml -apiVersion: kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_version description: The version of the Launcher and Osquery process @@ -220,22 +221,22 @@ spec: launcher: 0.3.0 osquery: 2.9.0 --- -apiVersion: kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_schedule description: Report performance stats for each file in the query schedule. query: select name, interval, executions, output_size, wall_time, (user_time/executions) as avg_user_time, (system_time/executions) as avg_system_time, average_memory, last_executed from osquery_schedule; --- -apiVersion: kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_info description: A heartbeat counter that reports general performance (CPU, memory) and version. query: select i.*, p.resident_size, p.user_time, p.system_time, time.minutes as counter from osquery_info i, processes p, time where p.pid = i.pid; --- -apiVersion: kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_events description: Report event publisher health and track event counters. @@ -247,8 +248,8 @@ spec: To define query packs, reference queries defined elsewhere by name. This is why the "name" of a query is so important. You can define many of these packs in many files. ```yaml -apiVersion: kolide.com/v1alpha1 -kind: OsqueryPack +apiVersion: v1 +kind: pack spec: name: osquery_monitoring targets: diff --git a/docs/dashboard/README.md b/docs/dashboard/README.md new file mode 100644 index 000000000..139b79801 --- /dev/null +++ b/docs/dashboard/README.md @@ -0,0 +1,9 @@ +Dashboard Documentation +========================= + +Kolide Fleet is an application that allows you to take advantage of the power of osquery in order to maintain constant insight into the state of your infrastructure (security, health, stability, performance, compliance, etc). The dashboard documentation contains documents on the following topics: + +## Using the Kolide Fleet Dashboard + +- For information on running osquery queries on hosts in your infrastructure, you can refer to the [Running Queries](./running-queries.md) page. +- For information on configuring SSO for logging in to Fleet, see the guide on [Configuring Single Sign On](./single-sign-on.md). diff --git a/docs/application/running-queries.md b/docs/dashboard/running-queries.md similarity index 100% rename from docs/application/running-queries.md rename to docs/dashboard/running-queries.md diff --git a/docs/application/single-sign-on.md b/docs/dashboard/single-sign-on.md similarity index 100% rename from docs/application/single-sign-on.md rename to docs/dashboard/single-sign-on.md diff --git a/docs/development/README.md b/docs/development/README.md index 991997008..b615efa99 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -3,6 +3,10 @@ Development Documentation The Fleet application is a Go API server which serves a React/Redux single-page application for the frontend. The development documentation contains documents on the following topics: +## Frequently Asked Questions + +For FAQs on common Fleet problems, see the [FAQ](./faq.md). + ## Building and contributing code - For documentation on building the Fleet source code, see the [Building The Code](./building-the-code.md) guide. @@ -37,4 +41,4 @@ make generate make test make ./build/launcher --help -``` \ No newline at end of file +``` diff --git a/docs/development/faq.md b/docs/development/faq.md new file mode 100644 index 000000000..14bf186e6 --- /dev/null +++ b/docs/development/faq.md @@ -0,0 +1,22 @@ +# Troubleshooting FAQ + +## Make errors + +### `dep: command not found` + +``` +/bin/bash: dep: command not found +make: *** [.deps] Error 127 +``` + +If you get the above error, you need to add `$GOPATH/bin` to your PATH. A quick fix is to run `export PATH=$GOPATH/bin:$PATH`. +See the Go language documentation for [workspaces](https://golang.org/doc/code.html#Workspaces) and [GOPATH](https://golang.org/doc/code.html#GOPATH) for a more indepth documentation. + +### `undefined: Asset` + +``` +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. diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md index 27dda04c0..6aa43e891 100644 --- a/docs/infrastructure/README.md +++ b/docs/infrastructure/README.md @@ -15,6 +15,10 @@ The Fleet server has a few dependencies. To learn more about installing the Flee ## Managing a Fleet server -Running the Fleet server is a relatively simple process. We're prepared a brief guide to help you manage and maintain your Fleet server. Check out the guide for setting up and running [Fleet on Ubuntu](./fleet-on-ubuntu.md) and [Fleet on CentOS](./fleet-on-centos.md). +We're prepared a brief guide to help you manage and maintain your Fleet server. Check out the guide for setting up and running [Fleet on Ubuntu](./fleet-on-ubuntu.md) and [Fleet on CentOS](./fleet-on-centos.md). For more information, you can also read the [Configuring The Fleet Binary](./configuring-the-fleet-binary.md) guide for information on how to configure and customize Fleet for your organization. + +## Working with osquery logs + +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. diff --git a/docs/application/working-with-osquery-logs.md b/docs/infrastructure/working-with-osquery-logs.md similarity index 100% rename from docs/application/working-with-osquery-logs.md rename to docs/infrastructure/working-with-osquery-logs.md diff --git a/examples/config-many-files/config.yml b/examples/config-many-files/config.yml index 65f6df4d4..39349e068 100644 --- a/examples/config-many-files/config.yml +++ b/examples/config-many-files/config.yml @@ -1,30 +1,68 @@ --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryOptions +apiVersion: v1 +kind: options spec: config: - distributed_interval: 3 - distributed_tls_max_attempts: 3 - logger_plugin: tls - logger_tls_endpoint: /api/v1/osquery/log - logger_tls_period: 10 + options: + distributed_interval: 3 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 10 + decorators: + load: + - "SELECT version FROM osquery_info" + - "SELECT uuid AS host_uuid FROM system_info" + always: + - "SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1" + interval: + 3600: "SELECT total_seconds AS uptime FROM uptime" overrides: - # Note configs in overrides take precedence over base configs + # Note configs in overrides take precedence over the default config defined + # under the config key above. With this config file, the base config would + # only be used for Windows hosts, while Mac and Linux hosts would pull + # these overrides. platforms: darwin: - disable_tables: chrome_extensions - docker_socket: /var/run/docker.sock - logger_tls_period: 60 - fim: - interval: 500 - groups: - - name: etc - paths: - - /etc/%% - - name: users - paths: - - /Users/%/Library/%% - - /Users/%/Documents/%% + options: + distributed_interval: 10 + distributed_tls_max_attempts: 10 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 300 + disable_tables: chrome_extensions + docker_socket: /var/run/docker.sock + file_paths: + users: + - /Users/%/Library/%% + - /Users/%/Documents/%% + etc: + - /etc/%% linux: - schedule_timeout: 60 - docker_socket: /etc/run/docker.sock + options: + distributed_interval: 10 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 60 + schedule_timeout: 60 + docker_socket: /etc/run/docker.sock + file_paths: + homes: + - /root/.ssh/%% + - /home/%/.ssh/%% + etc: + - /etc/%% + tmp: + - /tmp/%% + exclude_paths: + homes: + - /home/not_to_monitor/.ssh/%% + tmp: + - /tmp/too_many_events/ + decorators: + load: + - "SELECT * FROM cpuid" + - "SELECT * FROM docker_info" + interval: + 3600: "SELECT total_seconds AS uptime FROM uptime" diff --git a/examples/config-many-files/decorators.yml b/examples/config-many-files/decorators.yml deleted file mode 100644 index f3b7d8952..000000000 --- a/examples/config-many-files/decorators.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryDecorator -spec: - query: hostname - type: interval - interval: 10 ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryDecorator -spec: - query: uuid - type: load ---- -apiVersion: k8s.kolide.com/v1alpha -kind: OsqueryDecorator - query: instance_id - type: load diff --git a/examples/config-many-files/labels.yml b/examples/config-many-files/labels.yml index f07aaf8c6..ff1cb5f45 100644 --- a/examples/config-many-files/labels.yml +++ b/examples/config-many-files/labels.yml @@ -1,42 +1,14 @@ --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: all_hosts - query: always_true ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: macs - query: darwin_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: ubuntu - query: ubuntu_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: centos - query: centos_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: windows - query: windows_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel +apiVersion: v1 +kind: label spec: + name: pending_updates query: pending_updates platforms: - darwin --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel +apiVersion: v1 +kind: label spec: + name: slack_not_running query: slack_not_running diff --git a/examples/config-many-files/packs/osquery-monitoring.yml b/examples/config-many-files/packs/osquery-monitoring.yml index ebebf4339..b3c310ec0 100644 --- a/examples/config-many-files/packs/osquery-monitoring.yml +++ b/examples/config-many-files/packs/osquery-monitoring.yml @@ -1,11 +1,8 @@ --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryPack +apiVersion: v1 +kind: pack spec: name: osquery_monitoring - targets: - labels: - - all_hosts queries: - query: osquery_version name: osquery_version_snapshot @@ -20,6 +17,6 @@ spec: - query: osquery_events interval: 86400 removed: false - - query: oquery_info + - query: osquery_info interval: 600 removed: false diff --git a/examples/config-many-files/queries.yml b/examples/config-many-files/queries.yml index 6bf9dd49e..da81cb751 100644 --- a/examples/config-many-files/queries.yml +++ b/examples/config-many-files/queries.yml @@ -1,6 +1,6 @@ --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_version description: The version of the Launcher and Osquery process @@ -9,30 +9,30 @@ spec: launcher: 0.3.0 osquery: 2.9.0 --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_schedule description: Report performance stats for each file in the query schedule. query: select name, interval, executions, output_size, wall_time, (user_time/executions) as avg_user_time, (system_time/executions) as avg_system_time, average_memory, last_executed from osquery_schedule; --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_info description: A heartbeat counter that reports general performance (CPU, memory) and version. query: select i.*, p.resident_size, p.user_time, p.system_time, time.minutes as counter from osquery_info i, processes p, time where p.pid = i.pid; --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_events description: Report event publisher health and track event counters. query: select name, publisher, type, subscriptions, events, active from osquery_events; -apiVersion: k8s.kolide.com/v1alpha1 +apiVersion: v1 --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: docker_processes descriptions: The docker containers processes that are running on a system. @@ -43,38 +43,38 @@ spec: - linux - darwin --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: hostname query: select hostname from system_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: uuid query: select uuid from osquery_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: instance_id query: select instance_id from system_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: always_true query: select 1; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: pending_updates query: SELECT value from plist where path = "/Library/Preferences/ManagedInstalls.plist" and key = "PendingUpdateCount" and value > "0"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: slack_not_running query: > @@ -85,26 +85,26 @@ spec: WHERE name LIKE "%Slack%" ); --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: centos_hosts query: select 1 from os_version where platform = "centos"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: ubuntu_hosts query: select 1 from os_version where platform = "ubuntu"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: windows_hosts query: select 1 from os_version where platform = "windows"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: darwin_hosts query: select 1 from os_version where platform = "darwin"; diff --git a/examples/config-single-file.yml b/examples/config-single-file.yml index 1320ce341..7837e69f5 100644 --- a/examples/config-single-file.yml +++ b/examples/config-single-file.yml @@ -1,101 +1,90 @@ --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryOptions +apiVersion: v1 +kind: options spec: config: - distributed_interval: 3 - distributed_tls_max_attempts: 3 - logger_plugin: tls - logger_tls_endpoint: /api/v1/osquery/log - logger_tls_period: 10 + options: + distributed_interval: 3 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 10 + decorators: + load: + - "SELECT version FROM osquery_info" + - "SELECT uuid AS host_uuid FROM system_info" + always: + - "SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1" + interval: + 3600: "SELECT total_seconds AS uptime FROM uptime" overrides: - # Note configs in overrides take precedence over base configs + # Note configs in overrides take precedence over the default config defined + # under the config key above. With this config file, the base config would + # only be used for Windows hosts, while Mac and Linux hosts would pull + # these overrides. platforms: darwin: - disable_tables: chrome_extensions - docker_socket: /var/run/docker.sock - logger_tls_period: 60 - fim: - interval: 500 - groups: - - name: etc - paths: - - /etc/%% - - name: users - paths: - - /Users/%/Library/%% - - /Users/%/Documents/%% + options: + distributed_interval: 10 + distributed_tls_max_attempts: 10 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 300 + disable_tables: chrome_extensions + docker_socket: /var/run/docker.sock + file_paths: + users: + - /Users/%/Library/%% + - /Users/%/Documents/%% + etc: + - /etc/%% linux: - schedule_timeout: 60 - docker_socket: /etc/run/docker.sock + options: + distributed_interval: 10 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 60 + schedule_timeout: 60 + docker_socket: /etc/run/docker.sock + file_paths: + homes: + - /root/.ssh/%% + - /home/%/.ssh/%% + etc: + - /etc/%% + tmp: + - /tmp/%% + exclude_paths: + homes: + - /home/not_to_monitor/.ssh/%% + tmp: + - /tmp/too_many_events/ + decorators: + load: + - "SELECT * FROM cpuid" + - "SELECT * FROM docker_info" + interval: + 3600: "SELECT total_seconds AS uptime FROM uptime" --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryDecorator -spec: - query: hostname - type: interval - interval: 10 ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryDecorator -spec: - query: uuid - type: load ---- -apiVersion: k8s.kolide.com/v1alpha -kind: OsqueryDecorator - query: instance_id - type: load ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: all_hosts - query: always_true ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: macs - query: darwin_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: ubuntu - query: ubuntu_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: centos - query: centos_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel -spec: - name: windows - query: windows_hosts ---- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel +apiVersion: v1 +kind: label spec: + name: pending_updates query: pending_updates platforms: - darwin --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryLabel +apiVersion: v1 +kind: label spec: + name: slack_not_running query: slack_not_running --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryPack +apiVersion: v1 +kind: pack spec: name: osquery_monitoring - targets: - labels: - - all_hosts queries: - query: osquery_version name: osquery_version_snapshot @@ -110,12 +99,12 @@ spec: - query: osquery_events interval: 86400 removed: false - - query: oquery_info + - query: osquery_info interval: 600 removed: false --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_version description: The version of the Launcher and Osquery process @@ -124,30 +113,29 @@ spec: launcher: 0.3.0 osquery: 2.9.0 --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_schedule description: Report performance stats for each file in the query schedule. query: select name, interval, executions, output_size, wall_time, (user_time/executions) as avg_user_time, (system_time/executions) as avg_system_time, average_memory, last_executed from osquery_schedule; --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_info description: A heartbeat counter that reports general performance (CPU, memory) and version. query: select i.*, p.resident_size, p.user_time, p.system_time, time.minutes as counter from osquery_info i, processes p, time where p.pid = i.pid; --- -apiVersion: k8s.kolide.com/v1alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: osquery_events description: Report event publisher health and track event counters. query: select name, publisher, type, subscriptions, events, active from osquery_events; -apiVersion: k8s.kolide.com/v1alpha1 --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: docker_processes descriptions: The docker containers processes that are running on a system. @@ -158,38 +146,38 @@ spec: - linux - darwin --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: hostname query: select hostname from system_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: uuid query: select uuid from osquery_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: instance_id query: select instance_id from system_info; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: always_true query: select 1; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: pending_updates query: SELECT value from plist where path = "/Library/Preferences/ManagedInstalls.plist" and key = "PendingUpdateCount" and value > "0"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: slack_not_running query: > @@ -200,26 +188,26 @@ spec: WHERE name LIKE "%Slack%" ); --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: centos_hosts query: select 1 from os_version where platform = "centos"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: ubuntu_hosts query: select 1 from os_version where platform = "ubuntu"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: windows_hosts query: select 1 from os_version where platform = "windows"; --- -apiVersion: k8s.kolide.com/v1/alpha1 -kind: OsqueryQuery +apiVersion: v1 +kind: query spec: name: darwin_hosts query: select 1 from os_version where platform = "darwin"; diff --git a/frontend/components/decorators/DecoratorRows/DecoratorRow/DecoratorRow.jsx b/frontend/components/decorators/DecoratorRows/DecoratorRow/DecoratorRow.jsx deleted file mode 100644 index 92faee693..000000000 --- a/frontend/components/decorators/DecoratorRows/DecoratorRow/DecoratorRow.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import ClickableTableRow from 'components/ClickableTableRow'; -import Checkbox from 'components/forms/fields/Checkbox'; -import decoratorInterface from 'interfaces/decorators'; -import classnames from 'classnames'; -import moment from 'moment'; -import { isEqual } from 'lodash'; - - -const baseClass = 'decorator-row'; - -class DecoratorRow extends Component { - static propTypes = { - checked: PropTypes.bool, - onCheck: PropTypes.func, - onSelect: PropTypes.func, - onDoubleClick: PropTypes.func, - decorator: decoratorInterface, - selected: PropTypes.bool, - builtIn: PropTypes.bool, - }; - - shouldComponentUpdate (nextProps) { - if (isEqual(nextProps, this.props)) { - return false; - } - return true; - } - - onCheck = (value) => { - const { onCheck: handleCheck, decorator } = this.props; - return handleCheck(value, decorator.id); - } - - onSelect = () => { - const { onSelect: handleSelect, decorator } = this.props; - // built in can't be selected - if (decorator.built_in) { - return false; - } - return handleSelect(decorator); - } - - onDoubleClick = () => { - const { onDoubleClick: handleDoubleClick, decorator } = this.props; - if (decorator.built_in) { - return false; - } - return handleDoubleClick(decorator); - } - - render () { - const { onCheck, onSelect, onDoubleClick } = this; - const { selected, checked, decorator, builtIn } = this.props; - const { id, name, updated_at: updatedAt, query, type, interval } = decorator; - const lastModifiedDate = moment(updatedAt).format('MM/DD/YY'); - const rowClassName = classnames(baseClass, { - [`${baseClass}--selected`]: selected, - }); - return ( - - - - - {name} - {type} - {interval} - {lastModifiedDate} - {query} - - ); - } - -} - -export default DecoratorRow; diff --git a/frontend/components/decorators/DecoratorRows/DecoratorRow/_styles.scss b/frontend/components/decorators/DecoratorRows/DecoratorRow/_styles.scss deleted file mode 100644 index bb1d0b0ea..000000000 --- a/frontend/components/decorators/DecoratorRows/DecoratorRow/_styles.scss +++ /dev/null @@ -1,33 +0,0 @@ -.decorator-row { - line-height: 38px; - - &--selected { - background-color: $accent-light; - } - - &:hover { - cursor: pointer; - } - - &:active, - &:focus { - outline: none; - } - - td { - font-size: 14px; - - &:nth-child(2) { - font-weight: $bold; - } - - .form-field { - margin: 0; - } - } - - &__name { - @include ellipsis(120px); - display: table-cell; - } -} diff --git a/frontend/components/decorators/DecoratorRows/DecoratorRow/index.js b/frontend/components/decorators/DecoratorRows/DecoratorRow/index.js deleted file mode 100644 index 917c0771e..000000000 --- a/frontend/components/decorators/DecoratorRows/DecoratorRow/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorRow'; diff --git a/frontend/components/decorators/DecoratorRows/DecoratorRows.jsx b/frontend/components/decorators/DecoratorRows/DecoratorRows.jsx deleted file mode 100644 index 30741fc7a..000000000 --- a/frontend/components/decorators/DecoratorRows/DecoratorRows.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import Checkbox from 'components/forms/fields/Checkbox'; -import decoratorInterface from 'interfaces/decorators'; -import { includes } from 'lodash'; -import DecoratorRow from 'components/decorators/DecoratorRows/DecoratorRow'; - -const baseClass = 'decorator-rows'; - -class DecoratorRows extends Component { - static propTypes = { - decorators: PropTypes.arrayOf(decoratorInterface), - onCheckDecorator: PropTypes.func, - onCheckAll: PropTypes.func, - onSelectDecorator: PropTypes.func, - allChecked: PropTypes.bool, - onDoubleClick: PropTypes.func, - checkedDecoratorIDs: PropTypes.arrayOf(PropTypes.number), - selectedDecorator: decoratorInterface, - }; - - constructor (props) { - super(props); - this.state = { allDecoratorsChecked: false }; - } - - onCheck = (checked, id) => { - const { allDecoratorsChecked } = this.state; - const { onCheckDecorator } = this.props; - if (allDecoratorsChecked) { - this.setState({ allDecoratorsChecked: false }); - } - onCheckDecorator(checked, id); - } - - handleCheckAll = (checked) => { - const { onCheckAll } = this.props; - onCheckAll(checked); - } - - isChecked = (decorator) => { - const { checkedDecoratorIDs } = this.props; - return includes(checkedDecoratorIDs, decorator.id); - } - - render () { - const { - decorators, - allChecked, - onSelectDecorator, - onDoubleClick, - selectedDecorator, - } = this.props; - - return ( -
- - - - - - - - - - - - - {decorators.map((decorator) => { - return ( - - ); - })} - -
- - Decorator NameTypeIntervalLast ModifiedQuery
-
- ); - } -} - -export default DecoratorRows; diff --git a/frontend/components/decorators/DecoratorRows/_styles.scss b/frontend/components/decorators/DecoratorRows/_styles.scss deleted file mode 100644 index ae55c3781..000000000 --- a/frontend/components/decorators/DecoratorRows/_styles.scss +++ /dev/null @@ -1,55 +0,0 @@ -.decorator-rows { - background-color: $white; - border: 1px solid $accent-dark; - border-radius: 3px; - box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12); - box-sizing: border-box; - - &__table { - border-collapse: collapse; - width: 100%; - border-radius: 3px; - - thead { - height: 50px; - background-color: $bg-medium; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12); - - .form-field { - margin: 0; - } - - th { - font-size: 14px; - font-weight: $bold; - letter-spacing: -0.5px; - text-align: left; - color: $link; - padding: 15px 10px; - - &:nth-child(1) { - border-top-left-radius: 3px; - width: 20px; - } - - &:last-child { - border-top-right-radius: 3px; - } - } - } - - tbody { - td { - padding: 0 10px; - margin: 0; - border-bottom: 1px solid $accent-light; - - &:nth-child(1) { - text-align: center; - vertical-align: middle; - } - } - } - } - -} diff --git a/frontend/components/decorators/DecoratorRows/index.js b/frontend/components/decorators/DecoratorRows/index.js deleted file mode 100644 index 788e56857..000000000 --- a/frontend/components/decorators/DecoratorRows/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorRows'; diff --git a/frontend/components/decorators/DecoratorsPageWrapper/DecoratorsPageWrapper.jsx b/frontend/components/decorators/DecoratorsPageWrapper/DecoratorsPageWrapper.jsx deleted file mode 100644 index e450b0a05..000000000 --- a/frontend/components/decorators/DecoratorsPageWrapper/DecoratorsPageWrapper.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, PropTypes } from 'react'; - -class DecoratorsPageWrapper extends Component { - static propTypes = { - children: PropTypes.node, - }; - - render() { - const { children } = this.props; - - if (!children) { - return false; - } - - return children; - } -} - -export default DecoratorsPageWrapper; diff --git a/frontend/components/decorators/DecoratorsPageWrapper/index.js b/frontend/components/decorators/DecoratorsPageWrapper/index.js deleted file mode 100644 index cc5daf5be..000000000 --- a/frontend/components/decorators/DecoratorsPageWrapper/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorsPageWrapper'; diff --git a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.jsx b/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.jsx deleted file mode 100644 index 7559daf7b..000000000 --- a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { uniq } from 'lodash'; - -import Button from 'components/buttons/Button'; -import Dropdown from 'components/forms/fields/Dropdown'; -import dropdownOptionInterface from 'interfaces/dropdownOption'; -import Form from 'components/forms/Form'; -import formFieldInterface from 'interfaces/form_field'; -import Icon from 'components/icons/Icon'; -import InputField from 'components/forms/fields/InputField'; - -const fieldNames = ['name', 'value']; - -class ConfigOptionForm extends Component { - static propTypes = { - baseClass: PropTypes.string, - configNameOptions: PropTypes.arrayOf(dropdownOptionInterface), - fields: PropTypes.shape({ - name: formFieldInterface, - value: formFieldInterface, - }), - formData: PropTypes.shape({ - read_only: PropTypes.bool, - }).isRequired, - onRemove: PropTypes.func.isRequired, - }; - - handleRemove = () => { - const { formData, onRemove } = this.props; - - return onRemove(formData); - } - - render () { - const { baseClass, configNameOptions, fields, formData } = this.props; - const { handleRemove } = this; - const { name, read_only: readOnly, value } = formData; - const inputType = formData.type === 'int' ? 'number' : 'input'; - const options = uniq(configNameOptions.concat({ label: name, value: name, disabled: readOnly || false })); - const disabled = readOnly || !!(name && value); - - return ( -
- - - - - ); - } -} - -export default Form(ConfigOptionForm, { fields: fieldNames }); diff --git a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.tests.jsx b/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.tests.jsx deleted file mode 100644 index dacd1cdfb..000000000 --- a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/ConfigOptionForm.tests.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import expect, { createSpy, restoreSpies } from 'expect'; -import { mount } from 'enzyme'; -import { noop } from 'lodash'; - -import ConfigOptionForm from 'components/forms/ConfigOptionsForm/ConfigOptionForm'; -import { - itBehavesLikeAFormInputElement, - itBehavesLikeAFormDropdownElement, -} from 'test/helpers'; - -describe('ConfigOptionForm - form', () => { - afterEach(restoreSpies); - - it('renders form fields for the config option name and value', () => { - const configNameOptions = [{ label: 'My option', value: 'my_option' }]; - const form = mount( - - ); - - itBehavesLikeAFormDropdownElement(form, 'name'); - itBehavesLikeAFormInputElement(form, 'value'); - }); - - it('calls the onChangeFunc prop when the form updates', () => { - const spy = createSpy(); - const configNameOptions = [{ label: 'My option', value: 'my_option' }]; - const form = mount( - - ); - - itBehavesLikeAFormInputElement(form, 'value', 'InputField', 'new config option value'); - itBehavesLikeAFormDropdownElement(form, 'name'); - - expect(spy).toHaveBeenCalledWith('value', 'new config option value'); - expect(spy).toHaveBeenCalledWith('name', 'my_option'); - }); - - it('renders the input fields as disabled when the option is read_only or name and value are present', () => { - const formData = { name: 'My option', value: 'My value', read_only: false }; - const configNameOptions = [formData]; - const disabledForm = mount( - - ); - const enabledForm = mount( - - ); - const readOnlyForm = mount( - - ); - - const disabledNameField = disabledForm.find('Dropdown'); - const disabledValueField = disabledForm.find({ name: 'value' }); - const enabledNameField = enabledForm.find('Dropdown'); - const enabledValueField = enabledForm.find({ name: 'value' }); - const readOnlyNameField = readOnlyForm.find('Dropdown'); - const readOnlyValueField = readOnlyForm.find({ name: 'value' }); - - expect(disabledNameField.prop('disabled')).toEqual(true); - expect(disabledValueField.prop('disabled')).toEqual(false); - expect(enabledNameField.prop('disabled')).toEqual(false); - expect(enabledValueField.prop('disabled')).toEqual(false); - expect(readOnlyNameField.prop('disabled')).toEqual(true); - expect(readOnlyValueField.prop('disabled')).toEqual(true); - }); - - it('calls onRemove with the formdata when the ex icon is clicked', () => { - const formData = { name: 'My option', value: 'my_option', read_only: false }; - const configNameOptions = [formData]; - const spy = createSpy(); - const form = mount( - - ); - const exIcon = form.find('Button'); - - exIcon.simulate('click'); - - expect(spy).toHaveBeenCalledWith(formData); - }); -}); - diff --git a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/index.js b/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/index.js deleted file mode 100644 index a2ed0705a..000000000 --- a/frontend/components/forms/ConfigOptionsForm/ConfigOptionForm/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './ConfigOptionForm'; diff --git a/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.jsx b/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.jsx deleted file mode 100644 index 40abff2d2..000000000 --- a/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { noop } from 'lodash'; - -import ConfigOptionForm from 'components/forms/ConfigOptionsForm/ConfigOptionForm'; -import configOptionInterface from 'interfaces/config_option'; -import dropdownOptionInterface from 'interfaces/dropdownOption'; - -const baseClass = 'config-options-form'; - -class ConfigOptionsForm extends Component { - static propTypes = { - completedOptions: PropTypes.arrayOf(configOptionInterface), - configNameOptions: PropTypes.arrayOf(dropdownOptionInterface), - errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types - onRemoveOption: PropTypes.func.isRequired, - onFormUpdate: PropTypes.func.isRequired, - }; - - static defaultProps = { - errors: {}, - onRemoveOption: noop, - onFormUpdate: noop, - }; - - handleFormUpdate = (option) => { - return (fieldName, value) => { - const { onFormUpdate } = this.props; - const newOption = { ...option, [fieldName]: value }; - - return onFormUpdate(option, newOption); - }; - } - - renderConfigOptionForm = (option, idx) => { - const { configNameOptions, errors, onRemoveOption } = this.props; - const { handleFormUpdate } = this; - const configErrors = errors[option.id] || {}; - - return ( -
  • - -
  • - ); - } - - render () { - const { completedOptions } = this.props; - const { renderConfigOptionForm } = this; - - return ( -
    -
      -
    • - Option Name - Value -
    • - - {completedOptions.map((option, idx) => { - return renderConfigOptionForm(option, idx); - })} -
    -
    - ); - } -} - -export default ConfigOptionsForm; diff --git a/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.tests.jsx b/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.tests.jsx deleted file mode 100644 index a50fc5e0e..000000000 --- a/frontend/components/forms/ConfigOptionsForm/ConfigOptionsForm.tests.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import expect, { createSpy, restoreSpies } from 'expect'; -import { mount } from 'enzyme'; - -import ConfigOptionsForm from 'components/forms/ConfigOptionsForm'; -import { configOptionStub } from 'test/stubs'; -import { fillInFormInput } from 'test/helpers'; - -describe('ConfigOptionsForm - form', () => { - afterEach(restoreSpies); - - it('renders a ConfigOptionForm for each completed config option', () => { - const formWithOneOption = mount(); - const formWithTwoOptions = mount(); - - expect(formWithOneOption.find('ConfigOptionForm').length).toEqual(1); - expect(formWithTwoOptions.find('ConfigOptionForm').length).toEqual(2); - }); - - it('calls the onFormUpdate prop with the old and new option when the option is updated', () => { - const spy = createSpy(); - const form = mount(); - const configOptionFormInput = form.find('ConfigOptionForm').find('InputField'); - - fillInFormInput(configOptionFormInput.find('input'), 'updated value'); - - expect(spy).toHaveBeenCalledWith(configOptionStub, { ...configOptionStub, value: 'updated value' }); - }); - - describe('error rendering', () => { - it('sets errors on the ConfigOptionForm correctly when there are errors', () => { - const errors = { - [configOptionStub.id]: { name: 'Must be unique' }, - 10101: { name: 'Something went wrong' }, - }; - - const form = mount(); - const configOptionForm = form.find('ConfigOptionForm'); - - expect(configOptionForm.prop('serverErrors')).toEqual({ - name: 'Must be unique', - }); - }); - - it('sets errors on the ConfigOptionForm correctly when there are errors on a different object', () => { - const errors = { - 10101: { name: 'Something went wrong' }, - }; - - const form = mount(); - const configOptionForm = form.find('ConfigOptionForm'); - - expect(configOptionForm.prop('serverErrors')).toEqual({}); - }); - - it('sets errors on the ConfigOptionForm correctly when there are no errors', () => { - const errors = {}; - const form = mount(); - const configOptionForm = form.find('ConfigOptionForm'); - - expect(configOptionForm.prop('serverErrors')).toEqual({}); - }); - }); -}); diff --git a/frontend/components/forms/ConfigOptionsForm/_styles.scss b/frontend/components/forms/ConfigOptionsForm/_styles.scss deleted file mode 100644 index 1905d61fc..000000000 --- a/frontend/components/forms/ConfigOptionsForm/_styles.scss +++ /dev/null @@ -1,58 +0,0 @@ -.config-options-form { - clear: both; - padding-top: 25px; - - &__options { - @include clearfix; - margin: 0; - padding: 25px 0 0; - list-style: none; - border-top: 1px solid $accent-medium; - } - - &__option { - border-bottom: 1px dashed $accent-medium; - margin: 0 0 $pad-small; - } - - &__option-header { - span { - font-size: 15px; - font-weight: $normal; - line-height: 2.5; - letter-spacing: 0.6px; - color: $text-medium; - text-transform: uppercase; - display: inline-block; - } - - &-name { - padding: 0 50px 0 40px; - width: 300px; - } - } - - &__form { - @include display(flex); - - .form-field--dropdown { - width: 300px; - margin-right: 50px; - margin-left: 15px; - } - - .form-field--input { - @include flex-grow(1); - } - } - - &__remove { - height: 40px; - color: $alert; - font-size: 24px; - } - - &__field { - width: 100%; - } -} diff --git a/frontend/components/forms/ConfigOptionsForm/index.js b/frontend/components/forms/ConfigOptionsForm/index.js deleted file mode 100644 index 282fa176f..000000000 --- a/frontend/components/forms/ConfigOptionsForm/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './ConfigOptionsForm'; diff --git a/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.jsx b/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.jsx deleted file mode 100644 index e34c3c21a..000000000 --- a/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import Form from 'components/forms/Form'; -import KolideAce from 'components/KolideAce'; -import Dropdown from 'components/forms/fields/Dropdown'; -import InputField from 'components/forms/fields/InputField'; -import Button from 'components/buttons/Button'; -import formFieldInterface from 'interfaces/form_field'; -import validateQuery from 'components/forms/validators/validate_query'; -import { size } from 'lodash'; - -const baseClass = 'decorator-form'; - -const validate = (formData) => { - const errors = {}; - const { - error: queryError, - valid: queryValid, - } = validateQuery(formData.query); - if (!queryValid) { - errors.query = queryError; - } - if (formData.name == null || formData.name === '') { - errors.name = 'Name can not be empty'; - } - // interval value must be evenly divisible by 60 - if (formData.type === 'interval') { - if ((formData.interval % 60) !== 0) { - errors.interval = 'Interval must be evenly divisible by 60'; - } else if (formData.interval <= 0) { - errors.interval = 'Interval must be greater than zero'; - } - } - const valid = !size(errors); - return { valid, errors }; -}; - -class DecoratorForm extends Component { - static propTypes = { - fields: PropTypes.shape({ - id: formFieldInterface, - query: formFieldInterface, - interval: formFieldInterface, - type: formFieldInterface, - name: formFieldInterface, - built_in: formFieldInterface, - }), - handleCancel: PropTypes.func, - handleSubmit: PropTypes.func, - newDecorator: PropTypes.bool, - }; - - constructor (props) { - super(props); - this.state = { errors: {} }; - } - - render() { - const { handleSubmit, handleCancel, fields, newDecorator } = this.props; - const { type } = fields; - const { errors } = this.state; - const types = [ - { label: 'Load', value: 'load' }, - { label: 'Always', value: 'always' }, - { label: 'Interval', value: 'interval' }, - ]; - const formTitle = newDecorator ? 'New Osquery Decorator' : 'Edit Osquery Decorator'; - - - return ( -
    -

    {formTitle}

    - - -
    - - -
    -
    - - -
    - - ); - } -} - -export default Form(DecoratorForm, { - fields: ['id', 'name', 'type', 'query', 'interval', 'built_in'], - validate, -}); diff --git a/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.tests.jsx b/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.tests.jsx deleted file mode 100644 index 5aac52222..000000000 --- a/frontend/components/forms/decorators/DecoratorForm/DecoratorForm.tests.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import expect, { createSpy, restoreSpies } from 'expect'; -import { mount } from 'enzyme'; -import targetMock from 'test/target_mock'; -import { noop } from 'lodash'; -import DecoratorForm from './index'; - -describe('DecoratorForm - component', () => { - beforeEach(targetMock); - afterEach(restoreSpies); - - it('calls handle submit when validation passes', () => { - const submitSpy = createSpy(); - const formData = { - query: 'SELECT seconds FROM uptime;', - type: 'interval', - interval: 3600, - built_in: false, - name: 'Foo', - }; - const form = mount(); - const submitButton = form.find('.decorator-form__form-btn--submit'); - submitButton.simulate('click'); - expect(submitSpy).toHaveBeenCalled(); - expect(form.state()).toInclude({ - errors: {}, - formData: { built_in: false, interval: 3600, name: 'Foo', query: 'SELECT seconds FROM uptime;', type: 'interval' }, - }); - }); - - it('does not validate interval when decorator is load type', () => { - const submitSpy = createSpy(); - const formData = { - query: 'SELECT seconds FROM uptime;', - type: 'load', - interval: 3603, // this will fail if validated - built_in: false, - name: 'Foo', - }; - const form = mount(); - const submitButton = form.find('.decorator-form__form-btn--submit'); - submitButton.simulate('click'); - expect(submitSpy).toHaveBeenCalled(); - expect(form.state()).toInclude({ - errors: {}, - formData: { built_in: false, interval: 3603, name: 'Foo', query: 'SELECT seconds FROM uptime;', type: 'load' }, - }); - }); - - it('validation fails when interval value not divisible by 60 for interval decorators', () => { - const updateSpy = createSpy(); - const formData = { - query: 'SELECT seconds FROM uptime;', - type: 'interval', - interval: 3601, - built_in: false, - name: 'Foo', - }; - const form = mount(); - const submitButton = form.find('.decorator-form__form-btn--submit'); - submitButton.simulate('click'); - expect(updateSpy).toNotHaveBeenCalled(); - expect(form.state()).toInclude({ - errors: { - interval: 'Interval must be evenly divisible by 60', - description: null, - }, - }); - }); - - it('validation fails for malformed sql statement', () => { - const updateSpy = createSpy(); - const formData = { - query: 'xxxxx seconds FROM uptime;', - type: 'load', - interval: 0, - built_in: false, - name: 'Foo', - }; - const form = mount(); - const submitButton = form.find('.decorator-form__form-btn--submit'); - submitButton.simulate('click'); - expect(updateSpy).toNotHaveBeenCalled(); - expect(form.state()).toInclude({ - errors: { query: 'Syntax error found near WITH Clause (Statement)' }, - }); - }); -}); diff --git a/frontend/components/forms/decorators/DecoratorForm/_styles.scss b/frontend/components/forms/decorators/DecoratorForm/_styles.scss deleted file mode 100644 index b1c3327ef..000000000 --- a/frontend/components/forms/decorators/DecoratorForm/_styles.scss +++ /dev/null @@ -1,39 +0,0 @@ -.decorator-form { - &__wrapper { - padding: $base; - - h1 { - margin-bottom: 19px; - } - } - - &__dropdown { - width: 200px; - } - - &__name { - width: 300px; - } - - &__button-wrap { - text-align: right; - } - - &__inputs { - width: 100%; - float: left; - padding: 0; - box-sizing: border-box; - - } - - &__form-btn { - padding: inherit $base; - margin-left: 10px; - margin-top: 30px; - - &--submit { - float: right; - } - } -} diff --git a/frontend/components/forms/decorators/DecoratorForm/helpers.js b/frontend/components/forms/decorators/DecoratorForm/helpers.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/components/forms/decorators/DecoratorForm/index.js b/frontend/components/forms/decorators/DecoratorForm/index.js deleted file mode 100644 index a69d69353..000000000 --- a/frontend/components/forms/decorators/DecoratorForm/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorForm'; diff --git a/frontend/components/side_panels/DecoratorInfoSidePanel/DecoratorInfoSidePanel.jsx b/frontend/components/side_panels/DecoratorInfoSidePanel/DecoratorInfoSidePanel.jsx deleted file mode 100644 index fb32275eb..000000000 --- a/frontend/components/side_panels/DecoratorInfoSidePanel/DecoratorInfoSidePanel.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import Icon from 'components/icons/Icon'; -import SecondarySidePanelContainer from '../SecondarySidePanelContainer'; - -const baseClass = 'decorator-info-side-panel'; - -const DecoratorInfoSidePanel = () => { - return ( - -

    - -   - What are Decorators? -

    -

    - Decorator queries are used to add additional information to results and snapshot logs. - There are three types of decorator queries based on when and how you want to collect the decoration data. -

    -

    The types of decorators are:

    -
      -
    • - load: run these decorators when the configuration loads (or is reloaded). -
    • -
    • - always: run these decorators before each query in the schedule. -
    • -
    • - interval: run the decorator on a defined interval. The interval must be a multiple of 60. - If the interval period is not divisible by 60 validation will fail. -
    • -
    -

    - Each decorator query should return at most 1 row. A warning will be generated if more than 1 row is returned as - they will be forcefully ignored and constitute undefined behavior. - Each decorator query should be careful not to emit column collisions, this is also undefined behavior. -

    -

    - The command line flag decorators_top_level can be set to true to make decorator data populate as top - level key/value objects instead of being contained as a child of decorations. -

    - -
    - ); -}; - -export default DecoratorInfoSidePanel; diff --git a/frontend/components/side_panels/DecoratorInfoSidePanel/_styles.scss b/frontend/components/side_panels/DecoratorInfoSidePanel/_styles.scss deleted file mode 100644 index 15760f96e..000000000 --- a/frontend/components/side_panels/DecoratorInfoSidePanel/_styles.scss +++ /dev/null @@ -1,62 +0,0 @@ -.decorator-info-side-panel { - background-color: $white; - border-left: 1px solid $border-medium; - bottom: 0; - box-shadow: 2px 0 8px 0 rgba($black, 0.1); - box-sizing: border-box; - overflow: scroll; - padding: 30px; - - &__title { - font-size: 18px; - font-weight: $normal; - letter-spacing: 0.7px; - color: $text-dark; - border-bottom: 1px solid $accent-light; - padding-bottom: 8px; - margin: 0 0 4px; - } - - &__subtitle { - font-size: 16px; - letter-spacing: 0.7px; - color: $text-dark; - margin: 0 0 10px; - padding-top: 15px; - } - - p, - ul { - font-size: 13px; - line-height: 1.85; - letter-spacing: 0.5px; - color: #858495; - } - - dl { - dt { - font-weight: $bold; - font-size: 13px; - line-height: 1.85; - letter-spacing: 0.5px; - color: #858495; - - .kolidecon { - font-size: 20px; - margin-right: 5px; - } - - span { - vertical-align: 2px; - } - } - - dd { - font-size: 13px; - line-height: 1.85; - letter-spacing: 0.5px; - color: #858495; - margin-left: 30px; - } - } -} diff --git a/frontend/components/side_panels/DecoratorInfoSidePanel/index.js b/frontend/components/side_panels/DecoratorInfoSidePanel/index.js deleted file mode 100644 index f5c5dc266..000000000 --- a/frontend/components/side_panels/DecoratorInfoSidePanel/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorInfoSidePanel'; diff --git a/frontend/components/side_panels/SiteNavSidePanel/navItems.js b/frontend/components/side_panels/SiteNavSidePanel/navItems.js index aa2c89462..09705aeec 100644 --- a/frontend/components/side_panels/SiteNavSidePanel/navItems.js +++ b/frontend/components/side_panels/SiteNavSidePanel/navItems.js @@ -1,14 +1,5 @@ export default (admin) => { const adminNavItems = [ - { - icon: 'config', - name: 'Config', - location: { - regex: /^\/config/, - pathname: '/config/options', - }, - subItems: [], - }, { icon: 'admin', name: 'Admin', @@ -99,32 +90,6 @@ export default (admin) => { }, ], }, - { - icon: 'decorator', - name: 'Decorators', - location: { - pathname: '/decorators/manage', - regex: /^\/decorators/, - }, - subItems: [ - { - icon: 'decorator', - name: 'Manage Decorators', - location: { - pathname: '/decorators/manage', - regex: /\/decorators\/manage/, - }, - }, - { - icon: 'pencil', - name: 'New Decorator', - location: { - regex: /\/decorators\/new/, - pathname: '/decorators/new', - }, - }, - ], - }, { icon: 'help', name: 'Help', diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js index 3d4b37851..b272d91b7 100644 --- a/frontend/kolide/endpoints.js +++ b/frontend/kolide/endpoints.js @@ -1,8 +1,6 @@ export default { CHANGE_PASSWORD: '/v1/kolide/change_password', CONFIG: '/v1/kolide/config', - CONFIG_OPTIONS: '/v1/kolide/options', - CONFIG_OPTIONS_RESET: '/v1/kolide/options/reset', CONFIRM_EMAIL_CHANGE: (token) => { return `/v1/kolide/email/change/${token}`; }, @@ -36,5 +34,4 @@ export default { return `/v1/kolide/users/${id}/admin`; }, SSO: '/v1/kolide/sso', - DECORATORS: '/v1/kolide/decorators', }; diff --git a/frontend/kolide/entities/config_options.js b/frontend/kolide/entities/config_options.js deleted file mode 100644 index 79cd10127..000000000 --- a/frontend/kolide/entities/config_options.js +++ /dev/null @@ -1,24 +0,0 @@ -import endpoints from 'kolide/endpoints'; - -export default (client) => { - return { - loadAll: () => { - const { CONFIG_OPTIONS } = endpoints; - - return client.authenticatedGet(client._endpoint(CONFIG_OPTIONS)) - .then(response => response.options); - }, - update: (options) => { - const { CONFIG_OPTIONS } = endpoints; - - return client.authenticatedPatch(client._endpoint(CONFIG_OPTIONS), JSON.stringify({ options })) - .then(response => response.options); - }, - reset: () => { - const { CONFIG_OPTIONS_RESET } = endpoints; - - return client.authenticatedGet(client._endpoint(CONFIG_OPTIONS_RESET)) - .then(response => response.options); - }, - }; -}; diff --git a/frontend/kolide/entities/config_options.tests.js b/frontend/kolide/entities/config_options.tests.js deleted file mode 100644 index 177ce4689..000000000 --- a/frontend/kolide/entities/config_options.tests.js +++ /dev/null @@ -1,42 +0,0 @@ -import expect from 'expect'; -import nock from 'nock'; - -import { configOptionStub } from 'test/stubs'; -import Kolide from 'kolide'; -import mocks from 'test/mocks'; - -const { configOptions: configOptionMocks } = mocks; - -describe('Kolide - API client (config options)', () => { - afterEach(() => { - nock.cleanAll(); - Kolide.setBearerToken(null); - }); - - const bearerToken = 'valid-bearer-token'; - - describe('#loadAll', () => { - it('calls the appropriate endpoint with the correct parameters', () => { - const request = configOptionMocks.loadAll.valid(bearerToken); - - Kolide.setBearerToken(bearerToken); - return Kolide.configOptions.loadAll() - .then(() => { - expect(request.isDone()).toEqual(true); - }); - }); - }); - - describe('#update', () => { - it('calls the appropriate endpoint with the correct parameters', () => { - const options = [configOptionStub]; - const request = configOptionMocks.update.valid(bearerToken, options); - - Kolide.setBearerToken(bearerToken); - return Kolide.configOptions.update(options) - .then(() => { - expect(request.isDone()).toEqual(true); - }); - }); - }); -}); diff --git a/frontend/kolide/entities/decorators.js b/frontend/kolide/entities/decorators.js deleted file mode 100644 index f46f9b7fa..000000000 --- a/frontend/kolide/entities/decorators.js +++ /dev/null @@ -1,29 +0,0 @@ -import endpoints from 'kolide/endpoints'; - -export default (client) => { - return { - loadAll: () => { - const { DECORATORS } = endpoints; - return client.authenticatedGet(client._endpoint(DECORATORS)) - .then(response => response.decorators); - }, - create: (formData) => { - const { DECORATORS } = endpoints; - const request = { payload: formData }; - return client.authenticatedPost(client._endpoint(DECORATORS), JSON.stringify(request)) - .then(response => response.decorator); - }, - destroy: ({ id }) => { - const { DECORATORS } = endpoints; - const endpoint = `${client._endpoint(DECORATORS)}/${id}`; - return client.authenticatedDelete(endpoint); - }, - update: (formData) => { - const { DECORATORS } = endpoints; - const endpoint = `${client._endpoint(DECORATORS)}/${formData.id}`; - const request = { payload: formData }; - return client.authenticatedPatch(endpoint, JSON.stringify(request)) - .then(response => response.decorator); - }, - }; -}; diff --git a/frontend/kolide/entities/decorators.tests.js b/frontend/kolide/entities/decorators.tests.js deleted file mode 100644 index acb1266a1..000000000 --- a/frontend/kolide/entities/decorators.tests.js +++ /dev/null @@ -1,53 +0,0 @@ -import expect from 'expect'; -import nock from 'nock'; - -import Kolide from 'kolide'; -import decoratorsMocks from 'test/mocks/decorators_mocks'; - -describe('Kolide - api client (decorators)', () => { - afterEach(() => { - nock.cleanAll(); - Kolide.setBearerToken(null); - }); - - const bearerToken = 'valid-bearer-token'; - - describe('#loadAll', () => { - it('calls the appropriate endpoint with the correct parameters', () => { - const request = decoratorsMocks.loadAll.valid(bearerToken); - - Kolide.setBearerToken(bearerToken); - return Kolide.decorators.loadAll() - .then(() => { - expect(request.isDone()).toEqual(true); - }); - }); - }); - - describe('#create', () => { - it('calls the appropriate endpoint with the correct parameters', () => { - const query = 'SELECT FROM FOO;'; - const interval = 0; - const param = { name: 'foo', type: 'load', query, interval, built_in: false }; - const request = decoratorsMocks.create.valid(bearerToken, param); - Kolide.setBearerToken(bearerToken); - return Kolide.decorators.create(param) - .then(() => { - expect(request.isDone()).toEqual(true); - }); - }); - }); - - describe('#destroy', () => { - it('calls the appropriate endpoint with the correct parameters', () => { - const id = 1; - const param = { id }; - const request = decoratorsMocks.destroy.valid(bearerToken, param); - Kolide.setBearerToken(bearerToken); - return Kolide.decorators.destroy(param) - .then(() => { - expect(request.isDone()).toEqual(true); - }); - }); - }); -}); diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index 2a102ff48..c47c32a6c 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -2,7 +2,6 @@ import Base from 'kolide/base'; import Request from 'kolide/request'; import accountMethods from 'kolide/entities/account'; import configMethods from 'kolide/entities/config'; -import configOptionMethods from 'kolide/entities/config_options'; import hostMethods from 'kolide/entities/hosts'; import inviteMethods from 'kolide/entities/invites'; import labelMethods from 'kolide/entities/labels'; @@ -14,7 +13,6 @@ import statusLabelMethods from 'kolide/entities/status_labels'; import targetMethods from 'kolide/entities/targets'; import userMethods from 'kolide/entities/users'; import websocketMethods from 'kolide/websockets'; -import decoratorMethods from 'kolide/entities/decorators'; const DEFAULT_BODY = JSON.stringify({}); @@ -24,7 +22,6 @@ class Kolide extends Base { this.account = accountMethods(this); this.config = configMethods(this); - this.configOptions = configOptionMethods(this); this.hosts = hostMethods(this); this.invites = inviteMethods(this); this.labels = labelMethods(this); @@ -36,7 +33,6 @@ class Kolide extends Base { this.targets = targetMethods(this); this.users = userMethods(this); this.websockets = websocketMethods(this); - this.decorators = decoratorMethods(this); } authenticatedDelete (endpoint, overrideHeaders = {}) { diff --git a/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.jsx b/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.jsx deleted file mode 100644 index 6ef903858..000000000 --- a/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.jsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { differenceWith, find, filter, isEqual, noop } from 'lodash'; - -import Button from 'components/buttons/Button'; -import configOptionActions from 'redux/nodes/entities/config_options/actions'; -import ConfigOptionsForm from 'components/forms/ConfigOptionsForm'; -import Icon from 'components/icons/Icon'; -import configOptionInterface from 'interfaces/config_option'; -import debounce from 'utilities/debounce'; -import entityGetter from 'redux/utilities/entityGetter'; -import helpers from 'pages/config/ConfigOptionsPage/helpers'; -import { renderFlash } from 'redux/nodes/notifications/actions'; - -const baseClass = 'config-options-page'; -const DEFAULT_CONFIG_OPTION = { name: '', value: '' }; - -export class ConfigOptionsPage extends Component { - static propTypes = { - configOptions: PropTypes.arrayOf(configOptionInterface), - dispatch: PropTypes.func.isRequired, - loadingConfig: PropTypes.bool, - }; - - static defaultProps = { - configOptions: [], - dispatch: noop, - }; - - constructor (props) { - super(props); - - this.state = { - configOptions: [], - configOptionErrors: {}, - }; - } - - componentWillMount () { - const { configOptions, dispatch } = this.props; - - this.setState({ configOptions }); - - dispatch(configOptionActions.loadAll()); - - return false; - } - - componentWillReceiveProps ({ configOptions }) { - if (!isEqual(configOptions, this.state.configOptions)) { - this.setState({ configOptions }); - } - - return false; - } - - onAddNewOption = (evt) => { - evt.preventDefault(); - - const { configOptions } = this.state; - - if (find(configOptions, DEFAULT_CONFIG_OPTION)) { - return false; - } - - this.setState({ - configOptions: [ - ...configOptions, - DEFAULT_CONFIG_OPTION, - ], - }); - - return false; - } - - onOptionUpdate = (oldOption, newOption) => { - const { configOptions } = this.state; - const newConfigOptions = helpers.updatedConfigOptions({ oldOption, newOption, configOptions }); - - this.setState({ configOptions: newConfigOptions }); - - return false; - } - - onRemoveOption = (option) => { - const { configOptions } = this.state; - const configOptionsWithoutRemovedOption = filter(configOptions, o => !isEqual(o, option)); - - if (isEqual(option, DEFAULT_CONFIG_OPTION)) { - this.setState({ configOptions: configOptionsWithoutRemovedOption }); - } else { - this.setState({ - configOptions: [ - ...configOptionsWithoutRemovedOption, - { ...option, value: null }, - ], - }); - } - - return false; - } - - onResetConfigOptions = () => { - const { dispatch } = this.props; - - dispatch(configOptionActions.resetOptions()) - .then(() => { - dispatch(renderFlash('success', 'Options reset to defaults.')); - return false; - }) - .catch(() => { - dispatch(renderFlash('error', 'Options reset failed.')); - return false; - }); - return false; - } - - onSave = debounce(() => { - const { dispatch } = this.props; - const changedOptions = this.calculateChangedOptions(); - const { errors, valid } = this.validate(); - - if (!changedOptions.length) { - return false; - } - - if (!valid) { - this.setState({ configOptionErrors: errors }); - - return false; - } - - const formattedChangedOptions = helpers.formatOptionsForServer(changedOptions); - - dispatch(configOptionActions.update(formattedChangedOptions)) - .then(() => { - dispatch(renderFlash('success', 'Options updated!')); - - return false; - }) - .catch(() => { - dispatch(renderFlash('error', 'We were unable to update your config options')); - return false; - }); - - return false; - }) - - calculateChangedOptions = () => { - const { configOptions: stateConfigOptions } = this.state; - const { configOptions: propConfigOptions } = this.props; - const presentStateConfigOptions = filter(stateConfigOptions, o => o.name); - - return differenceWith(presentStateConfigOptions, propConfigOptions, isEqual); - } - - validate = () => { - const { configOptions: allConfigOptions } = this.state; - const changedConfigOptions = this.calculateChangedOptions(); - - return helpers.configErrorsFor(changedConfigOptions, allConfigOptions); - } - - render () { - const { configOptionErrors, configOptions } = this.state; - const { loadingConfig } = this.props; - const { onAddNewOption, onOptionUpdate, onRemoveOption, onResetConfigOptions, onSave } = this; - const availableOptions = filter(configOptions, option => option.value !== null); - - if (loadingConfig) { - return false; - } - - return ( -
    -
    -
    -

    Manage Additional Osquery Options

    -

    - Osquery allows you to set a number of configuration options (Osquery Documentation). - Since Kolide manages your Osquery configuration, you can set these additional desired - options on this screen. Some options that Kolide needs to function correctly will be ignored. -

    -
    -
    - - -
    -
    - - -
    - ); - } -} - -const mapStateToProps = (state) => { - const { entities: configOptions } = entityGetter(state).get('config_options'); - const { loading: loadingConfig } = state.entities.config_options; - - return { configOptions, loadingConfig }; -}; - -export default connect(mapStateToProps)(ConfigOptionsPage); diff --git a/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.tests.jsx b/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.tests.jsx deleted file mode 100644 index 441aec4c0..000000000 --- a/frontend/pages/config/ConfigOptionsPage/ConfigOptionsPage.tests.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import expect from 'expect'; -import { mount } from 'enzyme'; - -import { ConfigOptionsPage } from 'pages/config/ConfigOptionsPage/ConfigOptionsPage'; -import { configOptionStub } from 'test/stubs'; -import { fillInFormInput } from 'test/helpers'; - -describe('ConfigOptionsPage - component', () => { - const blankConfigOption = { name: '', value: '' }; - const props = { configOptions: [], loadingConfig: false }; - - describe('rendering', () => { - it('does not render when loading', () => { - const loadingProps = { ...props, loadingConfig: true }; - const page = mount(); - - expect(page.html()).toNotExist(); - }); - - it('renders when not loading the config', () => { - const page = mount(); - - expect(page.length).toEqual(1); - }); - - it('renders reset and save buttons', () => { - const page = mount(); - const buttons = page.find('Button'); - const resetButton = buttons.find('.config-options-page__reset-btn'); - const saveButton = buttons.find('.config-options-page__save-btn'); - - expect(resetButton.length).toEqual(1); - expect(saveButton.length).toEqual(1); - }); - }); - - describe('removing a config option', () => { - it('sets the option value to null in state', () => { - const page = mount(); - const removeBtn = page.find('ConfigOptionForm').find('Button').first(); - - expect(page.state('configOptions')).toEqual([configOptionStub]); - - removeBtn.simulate('click'); - - expect(page.state('configOptions')).toEqual([{ - ...configOptionStub, - value: null, - }]); - }); - }); - - describe('adding a config option', () => { - it('adds a blank option to state', () => { - const page = mount(); - const addBtn = page.find('Button').last(); - - expect(page.state('configOptions')).toEqual([configOptionStub]); - - addBtn.simulate('click'); - - expect(page.state('configOptions')).toEqual([ - configOptionStub, - blankConfigOption, - ]); - }); - - it('only allows one blank config option', () => { - const page = mount(); - const addBtn = page.find('Button').last(); - - expect(page.state('configOptions')).toEqual([configOptionStub]); - - addBtn.simulate('click'); - addBtn.simulate('click'); - - expect(page.state('configOptions')).toEqual([ - configOptionStub, - blankConfigOption, - ]); - }); - }); - - describe('updating a config option', () => { - it('updates the config option in state', () => { - const page = mount(); - const configOptionInput = page.find('ConfigOptionForm').find('InputField'); - - fillInFormInput(configOptionInput.find('input'), 'updated value'); - - expect(page.state('configOptions')).toEqual([ - { ...configOptionStub, value: 'updated value' }, - ]); - }); - }); -}); diff --git a/frontend/pages/config/ConfigOptionsPage/_styles.scss b/frontend/pages/config/ConfigOptionsPage/_styles.scss deleted file mode 100644 index 9bdf221dc..000000000 --- a/frontend/pages/config/ConfigOptionsPage/_styles.scss +++ /dev/null @@ -1,45 +0,0 @@ -.config-options-page { - padding: 30px; - padding-bottom: 120px; - margin-bottom: $pad-small; - - &__btn-wrapper { - min-width: 214px; - float: right; - } - - &__header-content { - max-width: 75%; - float: left; - } - - &__options-wrapper { - border-top: 1px solid $accent-medium; - padding-top: 20px; - } - - &__reset-btn { - margin-bottom: 32px; - } - - &__add-new { - color: $success; - - .kolidecon { - font-size: 24px; - vertical-align: text-bottom; - margin-right: 15px; - } - } - - h1 { - color: $text-dark; - font-size: 24px; - font-weight: $light; - } - - p { - color: $text-medium; - font-size: 15px; - } -} diff --git a/frontend/pages/config/ConfigOptionsPage/default_config_options.js b/frontend/pages/config/ConfigOptionsPage/default_config_options.js deleted file mode 100644 index dd9bc336d..000000000 --- a/frontend/pages/config/ConfigOptionsPage/default_config_options.js +++ /dev/null @@ -1,396 +0,0 @@ -const defaultConfigOptions = [ - { - id: 1, - name: 'disable_distributed', - type: 'bool', - value: false, - read_only: true, - }, - { - id: 2, - name: 'distributed_plugin', - type: 'string', - value: 'tls', - read_only: true, - }, - { - id: 3, - name: 'distributed_tls_read_endpoint', - type: 'string', - value: '/api/v1/osquery/distributed/read', - read_only: true, - }, - { - id: 4, - name: 'distributed_tls_write_endpoint', - type: 'string', - value: '/api/v1/osquery/distributed/write', - read_only: true, - }, - { - id: 5, - name: 'pack_delimiter', - type: 'string', - value: '/', - read_only: true, - }, - { - id: 6, - name: 'aws_access_key_id', - type: 'string', - value: null, - read_only: false, - }, - { - id: 7, - name: 'aws_firehose_period', - type: 'int', - value: null, - read_only: false, - }, - { - id: 8, - name: 'aws_firehose_stream', - type: 'string', - value: null, - read_only: false, - }, - { - id: 9, - name: 'aws_kinesis_period', - type: 'int', - value: null, - read_only: false, - }, - { - id: 10, - name: 'aws_kinesis_random_partition_key', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 11, - name: 'aws_kinesis_stream', - type: 'string', - value: null, - read_only: false, - }, - { - id: 12, - name: 'aws_profile_name', - type: 'string', - value: null, - read_only: false, - }, - { - id: 13, - name: 'aws_region', - type: 'string', - value: null, - read_only: false, - }, - { - id: 14, - name: 'aws_secret_access_key', - type: 'string', - value: null, - read_only: false, - }, - { - id: 15, - name: 'aws_sts_arn_role', - type: 'string', - value: null, - read_only: false, - }, - { - id: 16, - name: 'aws_sts_region', - type: 'string', - value: null, - read_only: false, - }, - { - id: 17, - name: 'aws_sts_session_name', - type: 'string', - value: null, - read_only: false, - }, - { - id: 18, - name: 'aws_sts_timeout', - type: 'int', - value: null, - read_only: false, - }, - { - id: 19, - name: 'buffered_log_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 20, - name: 'decorations_top_level', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 21, - name: 'disable_caching', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 22, - name: 'disable_database', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 23, - name: 'disable_decorators', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 24, - name: 'disable_events', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 25, - name: 'disable_kernel', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 26, - name: 'disable_logging', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 27, - name: 'disable_tables', - type: 'string', - value: null, - read_only: false, - }, - { - id: 28, - name: 'distributed_interval', - type: 'int', - value: 10, - read_only: false, - }, - { - id: 29, - name: 'distributed_tls_max_attempts', - type: 'int', - value: 3, - read_only: false, - }, - { - id: 30, - name: 'enable_foreign', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 31, - name: 'enable_monitor', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 32, - name: 'ephemeral', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 33, - name: 'events_expiry', - type: 'int', - value: null, - read_only: false, - }, - { - id: 34, - name: 'events_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 35, - name: 'events_optimize', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 36, - name: 'host_identifier', - type: 'string', - value: null, - read_only: false, - }, - { - id: 37, - name: 'logger_event_type', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 38, - name: 'logger_mode', - type: 'string', - value: null, - read_only: false, - }, - { - id: 39, - name: 'logger_path', - type: 'string', - value: null, - read_only: false, - }, - { - id: 40, - name: 'logger_plugin', - type: 'string', - value: 'tls', - read_only: false, - }, - { - id: 41, - name: 'logger_secondary_status_only', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 42, - name: 'logger_syslog_facility', - type: 'int', - value: null, - read_only: false, - }, - { - id: 43, - name: 'logger_tls_compress', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 44, - name: 'logger_tls_endpoint', - type: 'string', - value: '/api/v1/osquery/log', - read_only: false, - }, - { - id: 45, - name: 'logger_tls_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 46, - name: 'logger_tls_period', - type: 'int', - value: 10, - read_only: false, - }, - { - id: 47, - name: 'pack_refresh_interval', - type: 'int', - value: null, - read_only: false, - }, - { - id: 48, - name: 'read_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 49, - name: 'read_user_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 50, - name: 'schedule_default_interval', - type: 'int', - value: null, - read_only: false, - }, - { - id: 51, - name: 'schedule_splay_percent', - type: 'int', - value: null, - read_only: false, - }, - { - id: 52, - name: 'schedule_timeout', - type: 'int', - value: null, - read_only: false, - }, - { - id: 53, - name: 'utc', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 54, - name: 'value_max', - type: 'int', - value: null, - read_only: false, - }, - { - id: 55, - name: 'verbose', - type: 'bool', - value: null, - read_only: false, - }, - { - id: 56, - name: 'worker_threads', - type: 'int', - value: null, - read_only: false, - }, -]; - -export default defaultConfigOptions; diff --git a/frontend/pages/config/ConfigOptionsPage/helpers.js b/frontend/pages/config/ConfigOptionsPage/helpers.js deleted file mode 100644 index 1f5946ed3..000000000 --- a/frontend/pages/config/ConfigOptionsPage/helpers.js +++ /dev/null @@ -1,89 +0,0 @@ -import { filter, find, flatMap, size } from 'lodash'; -import replaceArrayItem from 'utilities/replace_array_item'; - -const configOptionDropdownOptions = (configOptions) => { - return flatMap(configOptions, (option) => { - if (option.value !== null) { - return []; - } - - return { - disabled: option.read_only || false, - label: option.name, - value: option.name, - }; - }); -}; - -const configErrorsFor = (changedOptions, allOptions) => { - const errors = {}; - - changedOptions.forEach((option) => { - const { id, name } = option; - const optionErrors = {}; - - if (!name) { - optionErrors.name = 'Must be present'; - } - - if (name) { - const configOptionsWithName = filter(allOptions, { name }); - - if (configOptionsWithName.length > 1) { - optionErrors.name = 'Must be unique'; - } - } - - if (size(optionErrors)) { - errors[id] = optionErrors; - } - }); - - const valid = !size(errors); - - return { errors, valid }; -}; - -const formatOptionsForServer = (options) => { - return options.map((option) => { - const { type, value } = option; - - if (value === null) { - return option; - } - - switch (type) { - case 'int': - return { ...option, value: Number(value) }; - case 'bool': - return { - ...option, - value: (value === 'true') || (value === true), - }; - case 'string': - return { ...option, value: String(value) }; - default: - return option; - } - }); -}; - -const updatedConfigOptions = ({ oldOption, newOption, configOptions }) => { - const existingConfigOption = find(configOptions, { name: newOption.name }); - const newValue = newOption.value || oldOption.value; - const updatedConfigOption = { ...existingConfigOption, name: newOption.name, value: newValue }; - - // we are making an update to the same option so only need to replace it - if (updatedConfigOption.id === oldOption.id) { - return replaceArrayItem(configOptions, oldOption, updatedConfigOption); - } - - // we are changing the option name so we need to remove the other - // option with the same name before replacing the current option - const filteredConfigOptions = filter(configOptions, o => o.id !== updatedConfigOption.id); - const option = { ...oldOption, value: null }; - - return replaceArrayItem(filteredConfigOptions, oldOption, updatedConfigOption).concat(option); -}; - -export default { configErrorsFor, configOptionDropdownOptions, formatOptionsForServer, updatedConfigOptions }; diff --git a/frontend/pages/config/ConfigOptionsPage/helpers.tests.js b/frontend/pages/config/ConfigOptionsPage/helpers.tests.js deleted file mode 100644 index a69bb4bda..000000000 --- a/frontend/pages/config/ConfigOptionsPage/helpers.tests.js +++ /dev/null @@ -1,135 +0,0 @@ -import expect from 'expect'; - -import { configOptionStub } from 'test/stubs'; -import helpers from 'pages/config/ConfigOptionsPage/helpers'; - -describe('ConfigOptionsPage - helpers', () => { - describe('#configOptionDropdownOptions', () => { - const configOptions = [ - configOptionStub, - { ...configOptionStub, id: 2, name: 'another_config_option' }, - { ...configOptionStub, id: 3, name: 'third_config_option', read_only: true }, - { id: 4, name: 'fourth_config_option', value: null, read_only: true }, - { id: 5, name: 'fifth_config_option', value: '' }, - { id: 6, name: 'sixth_config_option', value: null, read_only: false }, - ]; - - it('returns the available dropdown options', () => { - expect(helpers.configOptionDropdownOptions(configOptions)).toEqual([ - { label: 'fourth_config_option', value: 'fourth_config_option', disabled: true }, - { label: 'sixth_config_option', value: 'sixth_config_option', disabled: false }, - ]); - }); - }); - - describe('#configErrorsFor', () => { - it('validates presence of the config option name', () => { - const configOptionWithoutName = { id: 10, name: '', value: 'something' }; - const configOptionWithoutValue = { id: 11, name: 'something', value: '' }; - const configOptions = [configOptionWithoutName, configOptionWithoutValue]; - - expect(helpers.configErrorsFor(configOptions, configOptions)).toEqual({ - valid: false, - errors: { - 10: { name: 'Must be present' }, - }, - }); - }); - - it('validates uniqueness of config option names', () => { - const configOption1 = { id: 10, name: 'something', value: 'something' }; - const configOption2 = { id: 11, name: 'something', value: 'something' }; - const configOptions = [configOption1, configOption2]; - - expect(helpers.configErrorsFor([configOption1], configOptions)).toEqual({ - valid: false, - errors: { - 10: { name: 'Must be unique' }, - }, - }); - }); - - it('returns an empty object when the options are valid', () => { - const configOption1 = { id: 10, name: 'something', value: 'something' }; - const configOption2 = { id: 11, name: 'something else', value: 'something' }; - const configOptions = [configOption1, configOption2]; - - expect(helpers.configErrorsFor([configOption1], configOptions)).toEqual({ - valid: true, - errors: {}, - }); - }); - }); - - describe('#formatOptionsForServer', () => { - it('sets boolean type options correctly', () => { - const stringOption = { id: 1, type: 'bool', name: 'utc', value: 'true' }; - const boolOption = { id: 1, type: 'bool', name: 'utc', value: true }; - const nullOption = { id: 1, type: 'bool', name: 'utc', value: null }; - - expect(helpers.formatOptionsForServer([stringOption])).toEqual([ - { ...stringOption, value: true }, - ]); - expect(helpers.formatOptionsForServer([boolOption])).toEqual([boolOption]); - expect(helpers.formatOptionsForServer([nullOption])).toEqual([nullOption]); - }); - - it('sets int type options correctly', () => { - const stringOption = { id: 1, type: 'int', name: 'utc', value: '100' }; - const intOption = { id: 1, type: 'int', name: 'utc', value: 100 }; - const nullOption = { id: 1, type: 'bool', name: 'utc', value: null }; - - expect(helpers.formatOptionsForServer([stringOption])).toEqual([ - { ...stringOption, value: 100 }, - ]); - expect(helpers.formatOptionsForServer([intOption])).toEqual([intOption]); - expect(helpers.formatOptionsForServer([nullOption])).toEqual([nullOption]); - }); - - it('sets string type options correctly', () => { - const stringOption = { id: 1, type: 'string', name: 'utc', value: 'something' }; - const intOption = { id: 1, type: 'string', name: 'utc', value: 100 }; - const boolOption = { id: 1, type: 'string', name: 'utc', value: false }; - const nullOption = { id: 1, type: 'bool', name: 'utc', value: null }; - - expect(helpers.formatOptionsForServer([stringOption])).toEqual([stringOption]); - expect(helpers.formatOptionsForServer([nullOption])).toEqual([nullOption]); - expect(helpers.formatOptionsForServer([intOption])).toEqual([ - { ...intOption, value: '100' }, - ]); - expect(helpers.formatOptionsForServer([boolOption])).toEqual([ - { ...boolOption, value: 'false' }, - ]); - }); - }); - - describe('#updatedConfigOptions', () => { - it('sets the old options value to null when changing the option name', () => { - const oldOption = { id: 2, name: 'old_option', value: 100 }; - const newOption = { id: 3, name: 'new_option' }; - const configOptions = [oldOption, newOption]; - - expect(helpers.updatedConfigOptions({ oldOption, newOption: { name: 'new_option' }, configOptions })).toEqual([ - { ...newOption, value: 100 }, - { ...oldOption, value: null }, - ]); - }); - - it('updates the option value when the value changes', () => { - const option1 = { id: 2, name: 'old_option', value: 100 }; - const option2 = { id: 3, name: 'new_option', value: null }; - const configOptions = [option1, option2]; - - const updatedOptions = helpers.updatedConfigOptions({ - oldOption: option2, - newOption: { ...option2, value: 200 }, - configOptions, - }); - - expect(updatedOptions).toEqual([ - option1, - { ...option2, value: 200 }, - ]); - }); - }); -}); diff --git a/frontend/pages/config/ConfigOptionsPage/index.js b/frontend/pages/config/ConfigOptionsPage/index.js deleted file mode 100644 index 3a6e77475..000000000 --- a/frontend/pages/config/ConfigOptionsPage/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './ConfigOptionsPage'; diff --git a/frontend/pages/decorators/DecoratorPage/DecoratorPage.jsx b/frontend/pages/decorators/DecoratorPage/DecoratorPage.jsx deleted file mode 100644 index cc31edcf0..000000000 --- a/frontend/pages/decorators/DecoratorPage/DecoratorPage.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { noop } from 'lodash'; -import { push } from 'react-router-redux'; -import debounce from 'utilities/debounce'; -import decoratorActions from 'redux/nodes/entities/decorators/actions'; -import DecoratorForm from 'components/forms/decorators/DecoratorForm'; -import QuerySidePanel from 'components/side_panels/QuerySidePanel'; -import osqueryTableInterface from 'interfaces/osquery_table'; -import { renderFlash } from 'redux/nodes/notifications/actions'; -import { selectOsqueryTable } from 'redux/nodes/components/Decorators/actions'; -import { decoratorInterface } from 'interfaces/decorators'; -import entityGetter from 'redux/utilities/entityGetter'; - - -const baseClass = 'decorator-page'; - -export class DecoratorPage extends Component { - static propTypes = { - selectedOsqueryTable: osqueryTableInterface, - dispatch: PropTypes.func, - decorator: decoratorInterface, - newDecorator: PropTypes.bool, - }; - - onSubmitNew = debounce((formData) => { - const { dispatch } = this.props; - formData.interval = Number(formData.interval); - return dispatch(decoratorActions.create(formData)) - .then(() => { - dispatch(push('/decorators/manage')); - }) - .catch(() => false); - }) - - onSubmitUpdate = debounce((formData) => { - const { dispatch } = this.props; - formData.interval = Number(formData.interval); - return dispatch(decoratorActions.update(formData)) - .then(() => { - dispatch(push('/decorators/manage')); - }) - .catch(() => false); - }) - - onCancel = () => { - const { dispatch } = this.props; - dispatch(push('/decorators/manage')); - dispatch(renderFlash('success', 'Decorator canceled!')); - } - - onOsqueryTableSelect = (tableName) => { - const { dispatch } = this.props; - dispatch(selectOsqueryTable(tableName)); - return false; - } - - render() { - const { - onSubmitNew, - onSubmitUpdate, - onCancel, - onOsqueryTableSelect, - } = this; - - const { - selectedOsqueryTable, - decorator, - newDecorator, - } = this.props; - - const onSubmit = newDecorator ? onSubmitNew : onSubmitUpdate; - - return ( -
    -
    -
    - -
    -
    - -
    - ); - } -} - -const mapStateToProps = (state, ownProps) => { - const { queryText, selectedOsqueryTable } = state.components.Decorators; - const { id: decoratorID } = ownProps.params; - let decorator = { built_in: false, type: 'load', query: '', interval: 0, name: '' }; - let newDecorator = true; - if (decoratorID) { - decorator = entityGetter(state).get('decorators').findBy({ id: decoratorID }); - newDecorator = false; - } - return { - queryText, - selectedOsqueryTable, - decorator, - newDecorator, - }; -}; - -export default connect(mapStateToProps)(DecoratorPage); diff --git a/frontend/pages/decorators/DecoratorPage/_styles.scss b/frontend/pages/decorators/DecoratorPage/_styles.scss deleted file mode 100644 index d7516d56c..000000000 --- a/frontend/pages/decorators/DecoratorPage/_styles.scss +++ /dev/null @@ -1,25 +0,0 @@ -.decorator-page { - @at-root .has-sidebar > &__content { - @include display(flex); - @include flex-direction(column); - @include align-self(stretch); - margin-bottom: $pad-base; - } - - &__results { - @include display(flex); - @include flex-grow(1); - position: relative; - min-height: 400px; - } - - - &__wrapper { - padding: $base; - min-height: 90vh; - } - - &__title { - margin: 0 0 12px; - } -} diff --git a/frontend/pages/decorators/DecoratorPage/index.js b/frontend/pages/decorators/DecoratorPage/index.js deleted file mode 100644 index 498cdfeab..000000000 --- a/frontend/pages/decorators/DecoratorPage/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './DecoratorPage'; diff --git a/frontend/pages/decorators/ManageDecoratorsPage/ManageDecoratorsPage.jsx b/frontend/pages/decorators/ManageDecoratorsPage/ManageDecoratorsPage.jsx deleted file mode 100644 index 6d66480df..000000000 --- a/frontend/pages/decorators/ManageDecoratorsPage/ManageDecoratorsPage.jsx +++ /dev/null @@ -1,249 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { push } from 'react-router-redux'; -import NumberPill from 'components/NumberPill'; -import decoratorActions from 'redux/nodes/entities/decorators/actions'; -import DecoratorRows from 'components/decorators/DecoratorRows'; -import Modal from 'components/modals/Modal'; -import Button from 'components/buttons/Button'; -import DecoratorInfoSidePanel from 'components/side_panels/DecoratorInfoSidePanel'; -import decoratorInterface from 'interfaces/decorators'; -import entityGetter from 'redux/utilities/entityGetter'; -import { renderFlash } from 'redux/nodes/notifications/actions'; -import paths from 'router/paths'; -import { pull, get, isEmpty } from 'lodash'; - - -const baseClass = 'manage-decorators-page'; - -export class ManageDecoratorsPage extends Component { - static propTypes = { - dispatch: PropTypes.func, - decorators: PropTypes.arrayOf(decoratorInterface), - selectedDecorator: decoratorInterface, - } - - constructor(props) { - super(props); - this.state = { - allChecked: false, - checkedIDs: [], - showDeleteModal: false, - }; - } - - componentWillMount() { - const { dispatch } = this.props; - dispatch(decoratorActions.loadAll()) - .catch(() => false); - } - - onCheckDecorator = (checked, id) => { - const { checkedIDs } = this.state; - const newCheckedIDs = checked ? checkedIDs.concat(id) : pull(checkedIDs, id); - this.setState({ allChecked: false, checkedIDs: newCheckedIDs }); - } - - onCheckAll = (checked) => { - const { decorators } = this.props; - if (checked) { - const newCheckedIDs = decorators.filter((decorator) => { - return !decorator.built_in; - }).map((decorator) => { - return decorator.id; - }); - this.setState({ allChecked: true, checkedIDs: newCheckedIDs }); - return; - } - - this.setState({ allChecked: false, checkedIDs: [] }); - } - - onSelectDecorator = (decorator) => { - const { dispatch, selectedDecorator } = this.props; - // if selected decorator is clicked again, this will undo the selected status - if (selectedDecorator && (selectedDecorator.id === decorator.id)) { - dispatch(push('/decorators/manage')); - return false; - } - const path = { - pathname: '/decorators/manage', - query: { selectedDecorator: decorator.id }, - }; - dispatch(push(path)); - return false; - } - - onDoubleClick = (decorator) => { - const { dispatch } = this.props; - const path = `/decorators/${decorator.id}`; - dispatch(push(path)); - return false; - } - - onDeleteDecorators = (evt) => { - evt.preventDefault(); - const { checkedIDs } = this.state; - const { dispatch } = this.props; - const { destroy } = decoratorActions; - - const promises = checkedIDs.map((id: number) => { - return dispatch(destroy({ id })); - }); - return Promise.all(promises) - .then(() => { - dispatch(renderFlash('success', 'Successfully deleted selected decorators.')); - this.setState({ checkedIDs: [], showDeleteModal: false, allChecked: false }); - }) - .catch(() => { - dispatch(renderFlash('error', 'Something went wrong.')); - this.setState({ showDeleteModal: false }); - return false; - }); - } - - toggleDeleteModal = () => { - const { showDeleteModal } = this.state; - this.setState({ showDeleteModal: !showDeleteModal }); - return false; - } - - showNewQueryPage = () => { - const { dispatch } = this.props; - const { NEW_DECORATOR } = paths; - dispatch(push(NEW_DECORATOR)); - return false; - } - - showEditDecorator = () => { - const { selectedDecorator, dispatch } = this.props; - const path = `/decorators/${selectedDecorator.id}`; - dispatch(push(path)); - return false; - } - - renderDeleteConfirmationModel = () => { - const { showDeleteModal } = this.state; - if (!showDeleteModal) { - return false; - } - - const { toggleDeleteModal, onDeleteDecorators } = this; - return ( - -

    Are you sure that you want to delete the selected decorators?

    -
    - - -
    -
    - ); - } - - renderNewButton = () => { - return ( - - ); - } - - renderDeleteButton = () => { - return ( -
    - -
    - ); - } - - renderEditButton = () => { - return ( -
    - -
    - ); - } - - renderSidePanel = () => { - return ( - - ); - } - - renderButtons = () => { - const checkedCount = this.state.checkedIDs.length; - const { selectedDecorator } = this.props; - if (checkedCount) { - return this.renderDeleteButton(); - } - if (selectedDecorator) { - return this.renderEditButton(selectedDecorator.id); - } - return this.renderNewButton(); - } - - render() { - const { decorators, selectedDecorator } = this.props; - const { checkedIDs, allChecked } = this.state; - const { - onCheckDecorator, - onCheckAll, - renderDeleteConfirmationModel, - renderSidePanel, - onSelectDecorator, - onDoubleClick, - } = this; - - return ( -
    -
    -

    - Osquery Decorators -
    - {this.renderButtons()} -
    -

    - - -
    - {renderSidePanel()} - {renderDeleteConfirmationModel()} -
    - ); - } -} - -const mapStateToProps = (state, { location }) => { - const decoratorEntities = entityGetter(state).get('decorators'); - let { entities: decorators } = decoratorEntities; - decorators = decorators.filter((decorator) => { return !isEmpty(decorator); }); - const selectedDecoratorID = get(location, 'query.selectedDecorator'); - const selectedDecorator = selectedDecoratorID && decoratorEntities.findBy({ id: selectedDecoratorID }); - return { decorators, selectedDecorator }; -}; - -export default connect(mapStateToProps)(ManageDecoratorsPage); diff --git a/frontend/pages/decorators/ManageDecoratorsPage/_styles.scss b/frontend/pages/decorators/ManageDecoratorsPage/_styles.scss deleted file mode 100644 index f861a6eac..000000000 --- a/frontend/pages/decorators/ManageDecoratorsPage/_styles.scss +++ /dev/null @@ -1,33 +0,0 @@ -.manage-decorators-page { - &__wrapper { - padding: $base; - min-height: 90vh; - } - - &__title { - margin: 0 0 20px; - } - - &__buttons { - @include flex-grow(1); - } - - &__top-buttons { - @include display(flex); - float: right; - - .form-field { - margin: 0; - } - } - - &__modal-btn-wrap { - @include display(flex); - @include flex-direction(row-reverse); - - .button { - margin-left: 15px; - width: 120px; - } - } -} diff --git a/frontend/pages/decorators/ManageDecoratorsPage/index.js b/frontend/pages/decorators/ManageDecoratorsPage/index.js deleted file mode 100644 index 63a2fe941..000000000 --- a/frontend/pages/decorators/ManageDecoratorsPage/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './ManageDecoratorsPage'; diff --git a/frontend/redux/nodes/components/Decorators/actions.js b/frontend/redux/nodes/components/Decorators/actions.js deleted file mode 100644 index 2a5324c8d..000000000 --- a/frontend/redux/nodes/components/Decorators/actions.js +++ /dev/null @@ -1,36 +0,0 @@ -import { find } from 'lodash'; - -import { osqueryTables } from 'utilities/osquery_tables'; - -export const SELECT_DECORATOR_TABLE = 'SELECT_DECORATOR_TABLE'; -export const SET_DECORATOR_QUERY_TEXT = 'SET_DECORATOR_QUERY_TEXT'; -export const SET_SELECTED_DECORATOR_TARGETS = 'SET_SELECTED_DECORATOR_TARGETS'; -export const SET_SELECTED_DECORATOR_TARGETS_QUERY = 'SET_SELECTED_DECORATOR_TARGETS_QUERY'; -export const defaultSelectedOsqueryTable = find(osqueryTables, { name: 'uptime' }); -export const selectOsqueryTable = (tableName) => { - const lowerTableName = tableName.toLowerCase(); - const selectedOsqueryTable = find(osqueryTables, { name: lowerTableName }); - - return { - type: SELECT_DECORATOR_TABLE, - payload: { selectedOsqueryTable }, - }; -}; -export const setQueryText = (queryText) => { - return { - type: SET_DECORATOR_QUERY_TEXT, - payload: { queryText }, - }; -}; -export const setSelectedTargets = (selectedTargets) => { - return { - type: SET_SELECTED_DECORATOR_TARGETS, - payload: { selectedTargets }, - }; -}; -export const setSelectedTargetsQuery = (selectedTargetsQuery) => { - return { - type: SET_SELECTED_DECORATOR_TARGETS_QUERY, - payload: { selectedTargetsQuery }, - }; -}; diff --git a/frontend/redux/nodes/components/Decorators/reducer.js b/frontend/redux/nodes/components/Decorators/reducer.js deleted file mode 100644 index 8d87c45d0..000000000 --- a/frontend/redux/nodes/components/Decorators/reducer.js +++ /dev/null @@ -1,43 +0,0 @@ -import { - defaultSelectedOsqueryTable, - SELECT_DECORATOR_TABLE, - SET_DECORATOR_QUERY_TEXT, - SET_SELECTED_DECORATOR_TARGETS, - SET_SELECTED_DECORATOR_TARGETS_QUERY, -} from './actions'; - -export const initialState = { - queryText: 'SELECT total_seconds AS uptime FROM uptime', - selectedOsqueryTable: defaultSelectedOsqueryTable, - selectedTargets: [], - selectedTargetsQuery: '', -}; - -const reducer = (state = initialState, { type, payload }) => { - switch (type) { - case SELECT_DECORATOR_TABLE: - return { - ...state, - selectedOsqueryTable: payload.selectedOsqueryTable, - }; - case SET_DECORATOR_QUERY_TEXT: - return { - ...state, - queryText: payload.queryText, - }; - case SET_SELECTED_DECORATOR_TARGETS: - return { - ...state, - selectedTargets: payload.selectedTargets, - }; - case SET_SELECTED_DECORATOR_TARGETS_QUERY: - return { - ...state, - selectedTargetsQuery: payload.selectedTargetsQuery, - }; - default: - return state; - } -}; - -export default reducer; diff --git a/frontend/redux/nodes/components/reducer.js b/frontend/redux/nodes/components/reducer.js index 9e7e5f33f..af61b99b5 100644 --- a/frontend/redux/nodes/components/reducer.js +++ b/frontend/redux/nodes/components/reducer.js @@ -4,12 +4,10 @@ import ForgotPasswordPage from './ForgotPasswordPage/reducer'; import ManageHostsPage from './ManageHostsPage/reducer'; import QueryPages from './QueryPages/reducer'; import ResetPasswordPage from './ResetPasswordPage/reducer'; -import Decorators from './Decorators/reducer'; export default combineReducers({ ForgotPasswordPage, ManageHostsPage, QueryPages, ResetPasswordPage, - Decorators, }); diff --git a/frontend/redux/nodes/entities/base/schemas.js b/frontend/redux/nodes/entities/base/schemas.js index 9d24a8bf2..aac413e6e 100644 --- a/frontend/redux/nodes/entities/base/schemas.js +++ b/frontend/redux/nodes/entities/base/schemas.js @@ -1,7 +1,6 @@ import { Schema } from 'normalizr'; const campaignsSchema = new Schema('campaigns'); -const configOptionsSchema = new Schema('config_options'); const hostsSchema = new Schema('hosts'); const invitesSchema = new Schema('invites'); const labelsSchema = new Schema('labels'); @@ -10,11 +9,9 @@ const queriesSchema = new Schema('queries'); const scheduledQueriesSchema = new Schema('scheduled_queries'); const targetsSchema = new Schema('targets'); const usersSchema = new Schema('users'); -const decoratorsSchema = new Schema('decorators'); export default { CAMPAIGNS: campaignsSchema, - CONFIG_OPTIONS: configOptionsSchema, HOSTS: hostsSchema, INVITES: invitesSchema, LABELS: labelsSchema, @@ -23,5 +20,4 @@ export default { SCHEDULED_QUERIES: scheduledQueriesSchema, TARGETS: targetsSchema, USERS: usersSchema, - DECORATORS: decoratorsSchema, }; diff --git a/frontend/redux/nodes/entities/config_options/actions.js b/frontend/redux/nodes/entities/config_options/actions.js deleted file mode 100644 index efebba3dc..000000000 --- a/frontend/redux/nodes/entities/config_options/actions.js +++ /dev/null @@ -1,37 +0,0 @@ -import Kolide from 'kolide'; -import config from 'redux/nodes/entities/config_options/config'; -import { formatErrorResponse } from 'redux/nodes/entities/base/helpers'; - -const { actions } = config; - -export const RESET_OPTIONS_START = 'RESET_OPTIONS_START'; -export const RESET_OPTIONS_SUCCESS = 'RESET_OPTIONS_SUCCESS'; -export const RESET_OPTIONS_FAILURE = 'RESET_OPTIONS_FAILURE'; - -export const resetOptionsStart = { type: RESET_OPTIONS_START }; -export const resetOptionsSuccess = (configOptions) => { - return { type: RESET_OPTIONS_SUCCESS, payload: { configOptions } }; -}; -export const resetOptionsFailure = (errors) => { - return { type: RESET_OPTIONS_FAILURE, payload: { errors } }; -}; - -export const resetOptions = () => { - return (dispatch) => { - dispatch(resetOptionsStart); - return Kolide.configOptions.reset() - .then((opts) => { - return dispatch(resetOptionsSuccess(opts)); - }) - .catch((error) => { - const formattedErrors = formatErrorResponse(error); - dispatch(resetOptionsFailure(formattedErrors)); - throw formattedErrors; - }); - }; -}; - -export default { - ...actions, - resetOptions, -}; diff --git a/frontend/redux/nodes/entities/config_options/actions.tests.js b/frontend/redux/nodes/entities/config_options/actions.tests.js deleted file mode 100644 index 8c7168afe..000000000 --- a/frontend/redux/nodes/entities/config_options/actions.tests.js +++ /dev/null @@ -1,55 +0,0 @@ -import expect, { restoreSpies, spyOn } from 'expect'; - -import Kolide from 'kolide'; - -import { reduxMockStore } from 'test/helpers'; - -import { - resetOptions, - resetOptionsStart, - resetOptionsSuccess, -} from './actions'; - -const store = { entities: { config_options: {} } }; -const options = [ - { id: 1, name: 'option1', type: 'int', value: 10 }, - { id: 2, name: 'option2', type: 'string', value: 'wappa' }, -]; - -describe('Options - actions', () => { - describe('resetOptions', () => { - describe('successful request', () => { - beforeEach(() => { - spyOn(Kolide.configOptions, 'reset').andCall(() => { - return Promise.resolve(options); - }); - }); - - afterEach(restoreSpies); - - it('calls the API', () => { - const mockStore = reduxMockStore(store); - return mockStore.dispatch(resetOptions()) - .then(() => { - expect(Kolide.configOptions.reset).toHaveBeenCalled(); - }); - }); - - it('dispatches the correct actions', (done) => { - const mockStore = reduxMockStore(store); - mockStore.dispatch(resetOptions()) - .then(() => { - const dispatchedActions = mockStore.getActions(); - - expect(dispatchedActions).toEqual([ - resetOptionsStart, - resetOptionsSuccess(options), - ]); - - done(); - }) - .catch(done); - }); - }); - }); -}); diff --git a/frontend/redux/nodes/entities/config_options/config.js b/frontend/redux/nodes/entities/config_options/config.js deleted file mode 100644 index 1336d73ea..000000000 --- a/frontend/redux/nodes/entities/config_options/config.js +++ /dev/null @@ -1,13 +0,0 @@ -import Kolide from 'kolide'; -import Config from 'redux/nodes/entities/base/config'; -import schemas from 'redux/nodes/entities/base/schemas'; - -const { CONFIG_OPTIONS: schema } = schemas; - -export default new Config({ - entityName: 'config_options', - loadAllFunc: Kolide.configOptions.loadAll, - schema, - updateFunc: Kolide.configOptions.update, -}); - diff --git a/frontend/redux/nodes/entities/config_options/reducer.js b/frontend/redux/nodes/entities/config_options/reducer.js deleted file mode 100644 index 1c5de75ef..000000000 --- a/frontend/redux/nodes/entities/config_options/reducer.js +++ /dev/null @@ -1,35 +0,0 @@ -import { - RESET_OPTIONS_START, - RESET_OPTIONS_SUCCESS, - RESET_OPTIONS_FAILURE, -} from './actions'; - -import config, { initialState } from './config'; - -export default (state = initialState, { type, payload }) => { - switch (type) { - case RESET_OPTIONS_START: - return { - ...state, - errors: {}, - loading: true, - data: { - ...state.data, - }, - }; - case RESET_OPTIONS_SUCCESS: - return { - ...state, - errors: {}, - loading: false, - data: payload.configOptions, - }; - case RESET_OPTIONS_FAILURE: - return { - ...state, - errors: payload.errors, - }; - default: - return config.reducer(state, { type, payload }); - } -}; diff --git a/frontend/redux/nodes/entities/config_options/reducer.tests.js b/frontend/redux/nodes/entities/config_options/reducer.tests.js deleted file mode 100644 index b07b8e1b1..000000000 --- a/frontend/redux/nodes/entities/config_options/reducer.tests.js +++ /dev/null @@ -1,28 +0,0 @@ -import expect from 'expect'; -import reducer from './reducer'; -import { - resetOptionsSuccess, -} from './actions'; - -const resetOptions = [ - { id: 1, name: 'option1', type: 'int', value: 10 }, - { id: 2, name: 'option2', type: 'string', value: 'original' }, -]; - -describe('Options - reducer', () => { - describe('reset', () => { - it('should return options on success', () => { - const initState = { - loading: true, - errors: {}, - data: {}, - }; - const newState = reducer(initState, resetOptionsSuccess(resetOptions)); - expect(newState).toEqual({ - ...initState, - loading: false, - data: resetOptions, - }); - }); - }); -}); diff --git a/frontend/redux/nodes/entities/decorators/actions.js b/frontend/redux/nodes/entities/decorators/actions.js deleted file mode 100644 index b95c41eb8..000000000 --- a/frontend/redux/nodes/entities/decorators/actions.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config.actions; diff --git a/frontend/redux/nodes/entities/decorators/config.js b/frontend/redux/nodes/entities/decorators/config.js deleted file mode 100644 index a17bf97cd..000000000 --- a/frontend/redux/nodes/entities/decorators/config.js +++ /dev/null @@ -1,15 +0,0 @@ -import Kolide from 'kolide'; -import Config from 'redux/nodes/entities/base/config'; -import schemas from 'redux/nodes/entities/base/schemas'; - -const { DECORATORS: schema } = schemas; - - -export default new Config({ - entityName: 'decorators', - loadAllFunc: Kolide.decorators.loadAll, - createFunc: Kolide.decorators.create, - destroyFunc: Kolide.decorators.destroy, - updateFunc: Kolide.decorators.update, - schema, -}); diff --git a/frontend/redux/nodes/entities/decorators/reducer.js b/frontend/redux/nodes/entities/decorators/reducer.js deleted file mode 100644 index 93a1bd364..000000000 --- a/frontend/redux/nodes/entities/decorators/reducer.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config.reducer; diff --git a/frontend/redux/nodes/entities/reducer.js b/frontend/redux/nodes/entities/reducer.js index d05daec3c..b9ffcc892 100644 --- a/frontend/redux/nodes/entities/reducer.js +++ b/frontend/redux/nodes/entities/reducer.js @@ -1,7 +1,6 @@ import { combineReducers } from 'redux'; import campaigns from './campaigns/reducer'; -import configOptions from './config_options/reducer'; import hosts from './hosts/reducer'; import invites from './invites/reducer'; import labels from './labels/reducer'; @@ -9,11 +8,9 @@ import packs from './packs/reducer'; import queries from './queries/reducer'; import scheduledQueries from './scheduled_queries/reducer'; import users from './users/reducer'; -import decorators from './decorators/reducer'; export default combineReducers({ campaigns, - config_options: configOptions, hosts, invites, labels, @@ -21,5 +18,4 @@ export default combineReducers({ queries, scheduled_queries: scheduledQueries, users, - decorators, }); diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx index 31417dabd..1faf055d9 100644 --- a/frontend/router/index.jsx +++ b/frontend/router/index.jsx @@ -9,7 +9,6 @@ import AllPacksPage from 'pages/packs/AllPacksPage'; import App from 'components/App'; import AuthenticatedAdminRoutes from 'components/AuthenticatedAdminRoutes'; import AuthenticatedRoutes from 'components/AuthenticatedRoutes'; -import ConfigOptionsPage from 'pages/config/ConfigOptionsPage'; import ConfirmInvitePage from 'pages/ConfirmInvitePage'; import ConfirmSSOInvitePage from 'pages/ConfirmSSOInvitePage'; import CoreLayout from 'layouts/CoreLayout'; @@ -28,9 +27,6 @@ import Kolide404 from 'pages/Kolide404'; import Kolide500 from 'pages/Kolide500'; import store from 'redux/store'; import UserSettingsPage from 'pages/UserSettingsPage'; -import DecoratorPage from 'pages/decorators/DecoratorPage'; -import ManageDecoratorsPage from 'pages/decorators/ManageDecoratorsPage'; -import DecoratorsPageWrapper from 'components/decorators/DecoratorsPageWrapper'; const history = syncHistoryWithStore(browserHistory, store); @@ -54,17 +50,9 @@ const routes = ( - - - - - - - - diff --git a/frontend/router/paths.js b/frontend/router/paths.js index d76be4488..e77e96c91 100644 --- a/frontend/router/paths.js +++ b/frontend/router/paths.js @@ -13,7 +13,6 @@ export default { MANAGE_HOSTS: '/hosts/manage', NEW_PACK: '/packs/new', NEW_QUERY: '/queries/new', - NEW_DECORATOR: '/decorators/new', RESET_PASSWORD: '/login/reset', SETUP: '/setup', USER_SETTINGS: '/settings', diff --git a/frontend/styles/global/_icons.scss b/frontend/styles/global/_icons.scss index 4b8a14294..b4aa92bf6 100644 --- a/frontend/styles/global/_icons.scss +++ b/frontend/styles/global/_icons.scss @@ -376,10 +376,6 @@ content: '\f070'; } -.kolidecon-decorator:before { - content: '\f073'; -} - .sr-only { position: absolute; width: 1px; diff --git a/frontend/test/mocks/config_option_mocks.js b/frontend/test/mocks/config_option_mocks.js deleted file mode 100644 index 7417363a5..000000000 --- a/frontend/test/mocks/config_option_mocks.js +++ /dev/null @@ -1,35 +0,0 @@ -import createRequestMock from 'test/mocks/create_request_mock'; - -export default { - loadAll: { - valid: (bearerToken) => { - return createRequestMock({ - bearerToken, - endpoint: '/api/v1/kolide/options', - method: 'get', - response: { options: [] }, - }); - }, - }, - update: { - valid: (bearerToken, params) => { - return createRequestMock({ - bearerToken, - endpoint: '/api/v1/kolide/options', - method: 'patch', - params: { options: params }, - response: { options: params }, - }); - }, - }, - reset: { - valid: (bearerToken) => { - return createRequestMock({ - bearerToken, - endpoint: '/api/v1/kolide/options/reset', - method: 'get', - response: { options: [] }, - }); - }, - }, -}; diff --git a/frontend/test/mocks/decorators_mocks.js b/frontend/test/mocks/decorators_mocks.js deleted file mode 100644 index 79c2952f3..000000000 --- a/frontend/test/mocks/decorators_mocks.js +++ /dev/null @@ -1,38 +0,0 @@ -import createRequestMock from 'test/mocks/create_request_mock'; - - -export default { - loadAll: { - valid: (bearerToken) => { - return createRequestMock({ - bearerToken, - endpoint: '/api/v1/kolide/decorators', - method: 'get', - response: { decorators: [] }, - }); - }, - }, - create: { - valid: (bearerToken, params) => { - const req = { payload: params }; - return createRequestMock({ - bearerToken, - endpoint: '/api/v1/kolide/decorators', - method: 'post', - req, - response: { decorator: params }, - responseStatus: 201, - }); - }, - }, - destroy: { - valid: (bearerToken, { id }) => { - return createRequestMock({ - bearerToken, - endpoint: `/api/v1/kolide/decorators/${id}`, - method: 'delete', - response: {}, - }); - }, - }, -}; diff --git a/frontend/test/mocks/index.js b/frontend/test/mocks/index.js index ea6e79fee..36d1bd681 100644 --- a/frontend/test/mocks/index.js +++ b/frontend/test/mocks/index.js @@ -1,6 +1,5 @@ import account from 'test/mocks/account_mocks'; import config from 'test/mocks/config_mocks'; -import configOptions from 'test/mocks/config_option_mocks'; import hosts from 'test/mocks/host_mocks'; import invites from 'test/mocks/invite_mocks'; import labels from 'test/mocks/label_mocks'; @@ -16,7 +15,6 @@ import users from 'test/mocks/user_mocks'; export default { account, config, - configOptions, hosts, invites, labels, diff --git a/frontend/test/stubs.js b/frontend/test/stubs.js index 863693ed0..009584bb4 100644 --- a/frontend/test/stubs.js +++ b/frontend/test/stubs.js @@ -6,13 +6,6 @@ export const adminUserStub = { username: 'gnardog', }; -export const configOptionStub = { - id: 1, - name: 'config_option_name', - value: 'config option value', - read_only: false, -}; - export const configStub = { org_info: { org_name: 'Kolide', @@ -216,15 +209,6 @@ export const campaignStub = { }, }; -export const decoratorStub = { - id: 1, - query: 'SELECT version FROM osquery_info;', - type: 'load', - interval: 0, - built_in: 0, - name: 'foo', -}; - export default { adminUserStub, campaignStub, @@ -236,5 +220,4 @@ export default { queryStub, scheduledQueryStub, userStub, - decoratorStub, }; diff --git a/server/datastore/datastore_campaigns_test.go b/server/datastore/datastore_campaigns_test.go index 531a5e595..510603e1a 100644 --- a/server/datastore/datastore_campaigns_test.go +++ b/server/datastore/datastore_campaigns_test.go @@ -45,8 +45,18 @@ func testDistributedQueryCampaign(t *testing.T, ds kolide.Datastore) { h2 := test.NewHost(t, ds, "bar.local", "192.168.1.11", "2", "2", mockClock.Now().Add(-1*time.Hour)) h3 := test.NewHost(t, ds, "baz.local", "192.168.1.12", "3", "3", mockClock.Now().Add(-13*time.Minute)) - l1 := test.NewLabel(t, ds, "label foo", "query foo") - l2 := test.NewLabel(t, ds, "label bar", "query foo") + l1 := kolide.LabelSpec{ + ID: 1, + Name: "label foo", + Query: "query foo", + } + l2 := kolide.LabelSpec{ + ID: 2, + Name: "label bar", + Query: "query bar", + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1, &l2}) + require.Nil(t, err) checkTargets(t, ds, campaign.ID, []uint{}, []uint{}) diff --git a/server/datastore/datastore_decorators_test.go b/server/datastore/datastore_decorators_test.go deleted file mode 100644 index d125e748b..000000000 --- a/server/datastore/datastore_decorators_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package datastore - -import ( - "testing" - - "github.com/kolide/fleet/server/kolide" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func testDecorators(t *testing.T, ds kolide.Datastore) { - decorator := &kolide.Decorator{ - Query: "select from something", - Type: kolide.DecoratorInterval, - Interval: 60, - } - decorator, err := ds.NewDecorator(decorator) - require.Nil(t, err) - require.True(t, decorator.ID > 0) - result, err := ds.Decorator(decorator.ID) - require.Nil(t, err) - assert.Equal(t, decorator.Query, result.Query) - results, err := ds.ListDecorators() - require.Nil(t, err) - assert.Len(t, results, 1) - - decorator.Query = "select foo from bar;" - err = ds.SaveDecorator(decorator) - require.Nil(t, err) - result, err = ds.Decorator(decorator.ID) - require.Nil(t, err) - assert.Equal(t, "select foo from bar;", result.Query) - - err = ds.DeleteDecorator(decorator.ID) - require.Nil(t, err) - result, err = ds.Decorator(decorator.ID) - assert.NotNil(t, err) - -} diff --git a/server/datastore/datastore_hosts_test.go b/server/datastore/datastore_hosts_test.go index 6d3832b97..07c3a2ba1 100644 --- a/server/datastore/datastore_hosts_test.go +++ b/server/datastore/datastore_hosts_test.go @@ -2,6 +2,7 @@ package datastore import ( "fmt" + "sort" "strconv" "testing" "time" @@ -411,10 +412,12 @@ func testDistributedQueriesForHost(t *testing.T, ds kolide.Datastore) { assert.Empty(t, queries) // Create a label - l1, err := ds.NewLabel(&kolide.Label{ + l1 := kolide.LabelSpec{ + ID: 1, Name: "label foo", Query: "query1", - }) + } + err = ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1}) require.Nil(t, err) // Add hosts to label @@ -684,3 +687,22 @@ func testFlappingNetworkInterfaces(t *testing.T, ds kolide.Datastore) { require.Nil(t, err) assert.Len(t, host.NetworkInterfaces, 1) } + +func testHostIDsByName(t *testing.T, ds kolide.Datastore) { + for i := 0; i < 10; i++ { + _, err := ds.NewHost(&kolide.Host{ + DetailUpdateTime: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: fmt.Sprintf("host%d", i), + NodeKey: fmt.Sprintf("%d", i), + UUID: fmt.Sprintf("%d", i), + HostName: fmt.Sprintf("foo.%d.local", i), + }) + require.Nil(t, err) + } + + hosts, err := ds.HostIDsByName([]string{"foo.2.local", "foo.1.local", "foo.5.local"}) + require.Nil(t, err) + sort.Slice(hosts, func(i, j int) bool { return hosts[i] < hosts[j] }) + assert.Equal(t, hosts, []uint{2, 3, 6}) +} diff --git a/server/datastore/datastore_labels_test.go b/server/datastore/datastore_labels_test.go index 7aae36681..81804b3e0 100644 --- a/server/datastore/datastore_labels_test.go +++ b/server/datastore/datastore_labels_test.go @@ -39,35 +39,34 @@ func testLabels(t *testing.T, db kolide.Datastore) { assert.Nil(t, err) assert.Empty(t, queries) - newLabels := []kolide.Label{ + newLabels := []*kolide.LabelSpec{ // Note these are intentionally out of order - kolide.Label{ + &kolide.LabelSpec{ + ID: 1, Name: "label3", Query: "query3", Platform: "darwin", }, - kolide.Label{ + &kolide.LabelSpec{ + ID: 2, Name: "label1", Query: "query1", }, - kolide.Label{ + &kolide.LabelSpec{ + ID: 3, Name: "label2", Query: "query2", Platform: "darwin", }, - kolide.Label{ + &kolide.LabelSpec{ + ID: 4, Name: "label4", Query: "query4", Platform: "darwin", }, } - - for _, label := range newLabels { - var newLabel *kolide.Label - newLabel, err = db.NewLabel(&label) - assert.Nil(t, err) - assert.NotZero(t, newLabel.ID) - } + err = db.ApplyLabelSpecs(newLabels) + require.Nil(t, err) expectQueries := map[string]string{ "1": "query3", @@ -141,66 +140,87 @@ func testLabels(t *testing.T, db kolide.Datastore) { } func testManagingLabelsOnPacks(t *testing.T, ds kolide.Datastore) { - monitoringPack := &kolide.Pack{ - Name: "monitoring", + pack := &kolide.PackSpec{ + ID: 1, + Name: "pack1", } - _, err := ds.NewPack(monitoringPack) + err := ds.ApplyPackSpecs([]*kolide.PackSpec{pack}) require.Nil(t, err) - mysqlLabel := &kolide.Label{ + labels, err := ds.ListLabelsForPack(pack.ID) + require.Nil(t, err) + assert.Len(t, labels, 0) + + mysqlLabel := &kolide.LabelSpec{ + ID: 1, Name: "MySQL Monitoring", Query: "select pid from processes where name = 'mysqld';", } - mysqlLabel, err = ds.NewLabel(mysqlLabel) + err = ds.ApplyLabelSpecs([]*kolide.LabelSpec{mysqlLabel}) require.Nil(t, err) - err = ds.AddLabelToPack(mysqlLabel.ID, monitoringPack.ID) + pack.Targets = kolide.PackSpecTargets{ + Labels: []string{ + mysqlLabel.Name, + }, + } + err = ds.ApplyPackSpecs([]*kolide.PackSpec{pack}) require.Nil(t, err) - labels, err := ds.ListLabelsForPack(monitoringPack.ID) + labels, err = ds.ListLabelsForPack(pack.ID) require.Nil(t, err) if assert.Len(t, labels, 1) { assert.Equal(t, "MySQL Monitoring", labels[0].Name) } - osqueryLabel := &kolide.Label{ + osqueryLabel := &kolide.LabelSpec{ + ID: 2, Name: "Osquery Monitoring", Query: "select pid from processes where name = 'osqueryd';", } - osqueryLabel, err = ds.NewLabel(osqueryLabel) + err = ds.ApplyLabelSpecs([]*kolide.LabelSpec{mysqlLabel, osqueryLabel}) require.Nil(t, err) - err = ds.AddLabelToPack(osqueryLabel.ID, monitoringPack.ID) + pack.Targets = kolide.PackSpecTargets{ + Labels: []string{ + mysqlLabel.Name, + osqueryLabel.Name, + }, + } + err = ds.ApplyPackSpecs([]*kolide.PackSpec{pack}) require.Nil(t, err) - labels, err = ds.ListLabelsForPack(monitoringPack.ID) + labels, err = ds.ListLabelsForPack(pack.ID) require.Nil(t, err) assert.Len(t, labels, 2) - } func testSearchLabels(t *testing.T, db kolide.Datastore) { - _, err := db.NewLabel(&kolide.Label{ - Name: "foo", - }) + specs := []*kolide.LabelSpec{ + &kolide.LabelSpec{ + ID: 1, + Name: "foo", + }, + &kolide.LabelSpec{ + ID: 2, + Name: "bar", + }, + &kolide.LabelSpec{ + ID: 3, + Name: "foo-bar", + }, + &kolide.LabelSpec{ + ID: 4, + Name: "All Hosts", + LabelType: kolide.LabelTypeBuiltIn, + }, + } + err := db.ApplyLabelSpecs(specs) require.Nil(t, err) - _, err = db.NewLabel(&kolide.Label{ - Name: "bar", - }) + all, err := db.Label(specs[3].ID) require.Nil(t, err) - - l3, err := db.NewLabel(&kolide.Label{ - Name: "foo-bar", - }) - require.Nil(t, err) - - all, err := db.NewLabel(&kolide.Label{ - Name: "All Hosts", - LabelType: kolide.LabelTypeBuiltIn, - }) - require.Nil(t, err) - all, err = db.Label(all.ID) + l3, err := db.Label(specs[2].ID) require.Nil(t, err) // We once threw errors when the search query was empty. Verify that we @@ -230,16 +250,18 @@ func testSearchLabelsLimit(t *testing.T, db kolide.Datastore) { t.Skip("inmem is being deprecated, test skipped") } - _, err := db.NewLabel(&kolide.Label{ + all := &kolide.LabelSpec{ Name: "All Hosts", LabelType: kolide.LabelTypeBuiltIn, - }) + } + err := db.ApplyLabelSpecs([]*kolide.LabelSpec{all}) require.Nil(t, err) for i := 0; i < 15; i++ { - _, err := db.NewLabel(&kolide.Label{ - Name: fmt.Sprintf("foo-%d", i), - }) + l := &kolide.LabelSpec{ + Name: fmt.Sprintf("foo%d", i), + } + err := db.ApplyLabelSpecs([]*kolide.LabelSpec{l}) require.Nil(t, err) } @@ -279,12 +301,13 @@ func testListHostsInLabel(t *testing.T, db kolide.Datastore) { }) require.Nil(t, err) - l1, err := db.NewLabel(&kolide.Label{ + l1 := &kolide.LabelSpec{ + ID: 1, Name: "label foo", Query: "query1", - }) + } + err = db.ApplyLabelSpecs([]*kolide.LabelSpec{l1}) require.Nil(t, err) - require.NotZero(t, l1.ID) { @@ -332,19 +355,18 @@ func testListUniqueHostsInLabels(t *testing.T, db kolide.Datastore) { hosts = append(hosts, h) } - l1, err := db.NewLabel(&kolide.Label{ + l1 := kolide.LabelSpec{ + ID: 1, Name: "label foo", Query: "query1", - }) - require.Nil(t, err) - require.NotZero(t, l1.ID) - - l2, err := db.NewLabel(&kolide.Label{ + } + l2 := kolide.LabelSpec{ + ID: 2, Name: "label bar", Query: "query2", - }) + } + err := db.ApplyLabelSpecs([]*kolide.LabelSpec{&l1, &l2}) require.Nil(t, err) - require.NotZero(t, l2.ID) for i := 0; i < 3; i++ { err = db.RecordLabelQueryExecutions(hosts[i], map[uint]bool{l1.ID: true}, time.Now()) @@ -366,63 +388,88 @@ func testListUniqueHostsInLabels(t *testing.T, db kolide.Datastore) { } -func testSaveLabel(t *testing.T, db kolide.Datastore) { +func testChangeLabelDetails(t *testing.T, db kolide.Datastore) { if db.Name() == "inmem" { t.Skip("inmem is being deprecated") } - label := &kolide.Label{ + + label := kolide.LabelSpec{ + ID: 1, Name: "my label", Description: "a label", - Query: "select 1 from processes;", + Query: "select 1 from processes", Platform: "darwin", } - label, err := db.NewLabel(label) + err := db.ApplyLabelSpecs([]*kolide.LabelSpec{&label}) require.Nil(t, err) - label.Name = "changed name" + label.Description = "changed description" - _, err = db.SaveLabel(label) + err = db.ApplyLabelSpecs([]*kolide.LabelSpec{&label}) require.Nil(t, err) + saved, err := db.Label(label.ID) require.Nil(t, err) assert.Equal(t, label.Name, saved.Name) - assert.Equal(t, label.Description, saved.Description) } -func testReplaceDeletedLabel(t *testing.T, db kolide.Datastore) { - if db.Name() == "inmem" { - t.Skip("inmem is being deprecated, test skipped") +func setupLabelSpecsTest(t *testing.T, ds kolide.Datastore) []*kolide.LabelSpec { + expectedSpecs := []*kolide.LabelSpec{ + &kolide.LabelSpec{ + Name: "foo", + Query: "select * from foo", + Description: "foo description", + Platform: "darwin", + }, + &kolide.LabelSpec{ + Name: "bar", + Query: "select * from bar", + }, + &kolide.LabelSpec{ + Name: "bing", + Query: "select * from bing", + }, + &kolide.LabelSpec{ + Name: "All Hosts", + Query: "SELECT 1", + LabelType: kolide.LabelTypeBuiltIn, + }, } - - label := &kolide.Label{ - Name: "my label", - Query: "select 1 from processes;", - } - saved, err := db.NewLabel(label) + err := ds.ApplyLabelSpecs(expectedSpecs) require.Nil(t, err) - saved, err = db.Label(saved.ID) - require.Nil(t, err) - assert.Equal(t, label.Name, saved.Name) - assert.Equal(t, label.Description, saved.Description) - - newLabel := &kolide.Label{ - Name: "my label", - Query: " select * from time", - } - - // Replace should fail when label already exists and isn't soft deleted - _, err = db.NewLabel(newLabel) - require.NotNil(t, err) - - // Now delete label and replace should succeed - err = db.DeleteLabel(label.ID) - require.Nil(t, err) - - saved, err = db.NewLabel(newLabel) - require.Nil(t, err) - - saved, err = db.Label(saved.ID) - require.Nil(t, err) - assert.Equal(t, newLabel.Name, saved.Name) - assert.Equal(t, newLabel.Description, saved.Description) + return expectedSpecs +} + +func testGetLabelSpec(t *testing.T, ds kolide.Datastore) { + expectedSpecs := setupLabelSpecsTest(t, ds) + + for _, s := range expectedSpecs { + spec, err := ds.GetLabelSpec(s.Name) + require.Nil(t, err) + assert.Equal(t, s, spec) + } +} + +func testApplyLabelSpecsRoundtrip(t *testing.T, ds kolide.Datastore) { + expectedSpecs := setupLabelSpecsTest(t, ds) + + specs, err := ds.GetLabelSpecs() + require.Nil(t, err) + assert.Equal(t, expectedSpecs, specs) + + // Should be idempotent + err = ds.ApplyLabelSpecs(expectedSpecs) + require.Nil(t, err) + specs, err = ds.GetLabelSpecs() + require.Nil(t, err) + assert.Equal(t, expectedSpecs, specs) +} + +func testLabelIDsByName(t *testing.T, ds kolide.Datastore) { + setupLabelSpecsTest(t, ds) + + labels, err := ds.LabelIDsByName([]string{"foo", "bar", "bing"}) + require.Nil(t, err) + sort.Slice(labels, func(i, j int) bool { return labels[i] < labels[j] }) + assert.Equal(t, []uint{1, 2, 3}, labels) } diff --git a/server/datastore/datastore_osquery_options_test.go b/server/datastore/datastore_osquery_options_test.go new file mode 100644 index 000000000..6c7696052 --- /dev/null +++ b/server/datastore/datastore_osquery_options_test.go @@ -0,0 +1,95 @@ +package datastore + +import ( + "encoding/json" + "testing" + + "github.com/kolide/fleet/server/kolide" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testApplyOsqueryOptions(t *testing.T, ds kolide.Datastore) { + expectedOpts := &kolide.OptionsSpec{ + Config: json.RawMessage(`{"foo": "bar"}`), + Overrides: kolide.OptionsOverrides{ + Platforms: map[string]json.RawMessage{ + "darwin": json.RawMessage(`{"froob": "ling"}`), + }, + }, + } + + err := ds.ApplyOptions(expectedOpts) + require.Nil(t, err) + + retrievedOpts, err := ds.GetOptions() + require.Nil(t, err) + assert.Equal(t, expectedOpts, retrievedOpts) + + // Re-apply and verify everything has been replaced. + expectedOpts = &kolide.OptionsSpec{ + Config: json.RawMessage(`{"blue": "smurf"}`), + Overrides: kolide.OptionsOverrides{ + Platforms: map[string]json.RawMessage{ + "linux": json.RawMessage(`{"transitive": "nightfall"}`), + }, + }, + } + + err = ds.ApplyOptions(expectedOpts) + require.Nil(t, err) + + retrievedOpts, err = ds.GetOptions() + require.Nil(t, err) + assert.Equal(t, expectedOpts, retrievedOpts) +} + +func testApplyOsqueryOptionsNoOverrides(t *testing.T, ds kolide.Datastore) { + expectedOpts := &kolide.OptionsSpec{ + Config: json.RawMessage(`{}`), + } + + err := ds.ApplyOptions(expectedOpts) + require.Nil(t, err) + + retrievedOpts, err := ds.GetOptions() + require.Nil(t, err) + assert.Equal(t, expectedOpts.Config, retrievedOpts.Config) + assert.Empty(t, retrievedOpts.Overrides.Platforms) +} + +func testOsqueryOptionsForHost(t *testing.T, ds kolide.Datastore) { + defaultOpts := json.RawMessage(`{"foo": "bar"}`) + darwinOpts := json.RawMessage(`{"darwin": "macintosh"}`) + linuxOpts := json.RawMessage(`{"linux": "FOSS"}`) + expectedOpts := &kolide.OptionsSpec{ + Config: defaultOpts, + Overrides: kolide.OptionsOverrides{ + Platforms: map[string]json.RawMessage{ + "darwin": darwinOpts, + "linux": linuxOpts, + }, + }, + } + + err := ds.ApplyOptions(expectedOpts) + require.Nil(t, err) + + var testCases = []struct { + host kolide.Host + expectedOpts json.RawMessage + }{ + {kolide.Host{Platform: "windows"}, defaultOpts}, + {kolide.Host{Platform: "linux"}, linuxOpts}, + {kolide.Host{Platform: "darwin"}, darwinOpts}, + {kolide.Host{Platform: "some_other_platform"}, defaultOpts}, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + opts, err := ds.OptionsForPlatform(tt.host.Platform) + require.Nil(t, err) + assert.Equal(t, tt.expectedOpts, opts) + }) + } +} diff --git a/server/datastore/datastore_packs_test.go b/server/datastore/datastore_packs_test.go index 616450ed5..dca7652c2 100644 --- a/server/datastore/datastore_packs_test.go +++ b/server/datastore/datastore_packs_test.go @@ -11,17 +11,13 @@ import ( ) func testDeletePack(t *testing.T, ds kolide.Datastore) { - pack := &kolide.Pack{ - Name: "foo", - } - _, err := ds.NewPack(pack) - assert.Nil(t, err) + pack := test.NewPack(t, ds, "foo") assert.NotEqual(t, uint(0), pack.ID) - pack, err = ds.Pack(pack.ID) + pack, err := ds.Pack(pack.ID) require.Nil(t, err) - err = ds.DeletePack(pack.ID) + err = ds.DeletePack(pack.Name) assert.Nil(t, err) assert.NotEqual(t, uint(0), pack.ID) @@ -30,11 +26,7 @@ func testDeletePack(t *testing.T, ds kolide.Datastore) { } func testGetPackByName(t *testing.T, ds kolide.Datastore) { - pack := &kolide.Pack{ - Name: "foo", - } - _, err := ds.NewPack(pack) - assert.Nil(t, err) + pack := test.NewPack(t, ds, "foo") assert.NotEqual(t, uint(0), pack.ID) pack, ok, err := ds.PackByName(pack.Name) @@ -50,47 +42,62 @@ func testGetPackByName(t *testing.T, ds kolide.Datastore) { } -func testGetHostsInPack(t *testing.T, ds kolide.Datastore) { +func testListPacks(t *testing.T, ds kolide.Datastore) { + p1 := &kolide.PackSpec{ + ID: 1, + Name: "foo_pack", + } + p2 := &kolide.PackSpec{ + ID: 2, + Name: "bar_pack", + } + err := ds.ApplyPackSpecs([]*kolide.PackSpec{p1}) + require.Nil(t, err) + + packs, err := ds.ListPacks(kolide.ListOptions{}) + require.Nil(t, err) + assert.Len(t, packs, 1) + + err = ds.ApplyPackSpecs([]*kolide.PackSpec{p1, p2}) + require.Nil(t, err) + + packs, err = ds.ListPacks(kolide.ListOptions{}) + require.Nil(t, err) + assert.Len(t, packs, 2) +} + +func testListHostsInPack(t *testing.T, ds kolide.Datastore) { if ds.Name() == "inmem" { t.Skip("inmem is deprecated") } - user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) - mockClock := clock.NewMockClock() - p1, err := ds.NewPack(&kolide.Pack{ + l1 := kolide.LabelSpec{ + ID: 1, Name: "foo", - }) + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1}) require.Nil(t, err) - q1, err := ds.NewQuery(&kolide.Query{ - Name: "foo", - Query: "foo", - AuthorID: user.ID, - }) - require.Nil(t, err) - - q2, err := ds.NewQuery(&kolide.Query{ - Name: "bar", - Query: "bar", - AuthorID: user.ID, - }) - require.Nil(t, err) - - test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, false, false) - - l1, err := ds.NewLabel(&kolide.Label{ - Name: "foo", - }) - require.Nil(t, err) - - err = ds.AddLabelToPack(l1.ID, p1.ID) + p1 := &kolide.PackSpec{ + ID: 1, + Name: "foo_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + l1.Name, + }, + }, + } + err = ds.ApplyPackSpecs([]*kolide.PackSpec{p1}) require.Nil(t, err) h1 := test.NewHost(t, ds, "h1.local", "10.10.10.1", "1", "1", mockClock.Now()) + hostsInPack, err := ds.ListHostsInPack(p1.ID, kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, hostsInPack, 0) + err = ds.RecordLabelQueryExecutions( h1, map[uint]bool{l1.ID: true}, @@ -98,7 +105,7 @@ func testGetHostsInPack(t *testing.T, ds kolide.Datastore) { ) require.Nil(t, err) - hostsInPack, err := ds.ListHostsInPack(p1.ID, kolide.ListOptions{}) + hostsInPack, err = ds.ListHostsInPack(p1.ID, kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hostsInPack, 1) @@ -118,36 +125,309 @@ func testGetHostsInPack(t *testing.T, ds kolide.Datastore) { hostsInPack, err = ds.ListHostsInPack(p1.ID, kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hostsInPack, 2) - - h3 := test.NewHost(t, ds, "h3.local", "10.10.10.3", "3", "3", mockClock.Now()) - - err = ds.AddHostToPack(h3.ID, p1.ID) - require.Nil(t, err) - - hostsInPack, err = ds.ListHostsInPack(p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hostsInPack, 3) - - explicitHostsInPack, err = ds.ListExplicitHostsInPack(p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, explicitHostsInPack, 1) } func testAddLabelToPackTwice(t *testing.T, ds kolide.Datastore) { - l1 := test.NewLabel(t, ds, "l1", "select 1;") - p1 := test.NewPack(t, ds, "p1") + l1 := kolide.LabelSpec{ + ID: 1, + Name: "l1", + Query: "select 1", + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1}) + require.Nil(t, err) - err := ds.AddLabelToPack(l1.ID, p1.ID) - assert.Nil(t, err) - - labels, err := ds.ListLabelsForPack(p1.ID) - assert.Nil(t, err) - assert.Len(t, labels, 1) - - err = ds.AddLabelToPack(l1.ID, p1.ID) - assert.Nil(t, err) - - labels, err = ds.ListLabelsForPack(p1.ID) - assert.Nil(t, err) - assert.Len(t, labels, 1) + p1 := &kolide.PackSpec{ + ID: 1, + Name: "pack1", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + l1.Name, + l1.Name, + }, + }, + } + err = ds.ApplyPackSpecs([]*kolide.PackSpec{p1}) + require.NotNil(t, err) +} + +func setupPackSpecsTest(t *testing.T, ds kolide.Datastore) []*kolide.PackSpec { + zwass := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) + queries := []*kolide.Query{ + {Name: "foo", Description: "get the foos", Query: "select * from foo"}, + {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, + } + // Zach creates some queries + err := ds.ApplyQueries(zwass.ID, queries) + require.Nil(t, err) + + labels := []*kolide.LabelSpec{ + &kolide.LabelSpec{ + Name: "foo", + Query: "select * from foo", + }, + &kolide.LabelSpec{ + Name: "bar", + Query: "select * from bar", + }, + &kolide.LabelSpec{ + Name: "bing", + Query: "select * from bing", + }, + } + err = ds.ApplyLabelSpecs(labels) + require.Nil(t, err) + + boolPtr := func(b bool) *bool { return &b } + uintPtr := func(x uint) *uint { return &x } + stringPtr := func(s string) *string { return &s } + expectedSpecs := []*kolide.PackSpec{ + &kolide.PackSpec{ + ID: 1, + Name: "test_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + "foo", + "bar", + "bing", + }, + }, + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Description: "test_foo", + Interval: 42, + }, + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Name: "foo_snapshot", + Interval: 600, + Snapshot: boolPtr(true), + }, + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Interval: 600, + Removed: boolPtr(false), + Shard: uintPtr(73), + Platform: stringPtr("foobar"), + Version: stringPtr("0.0.0.0.0.1"), + }, + }, + }, + } + + err = ds.ApplyPackSpecs(expectedSpecs) + require.Nil(t, err) + return expectedSpecs +} + +func testApplyPackSpecRoundtrip(t *testing.T, ds kolide.Datastore) { + expectedSpecs := setupPackSpecsTest(t, ds) + + gotSpec, err := ds.GetPackSpecs() + require.Nil(t, err) + assert.Equal(t, expectedSpecs, gotSpec) +} + +func testGetPackSpec(t *testing.T, ds kolide.Datastore) { + expectedSpecs := setupPackSpecsTest(t, ds) + + for _, s := range expectedSpecs { + spec, err := ds.GetPackSpec(s.Name) + require.Nil(t, err) + assert.Equal(t, s, spec) + } +} + +func testApplyPackSpecMissingQueries(t *testing.T, ds kolide.Datastore) { + // Do not define queries mentioned in spec + specs := []*kolide.PackSpec{ + &kolide.PackSpec{ + ID: 1, + Name: "test_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{}, + }, + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: "bar", + Interval: 600, + }, + }, + }, + } + + // Should error due to unkown query + err := ds.ApplyPackSpecs(specs) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "unknown query 'bar'") + } +} + +func testListLabelsForPack(t *testing.T, ds kolide.Datastore) { + labelSpecs := []*kolide.LabelSpec{ + &kolide.LabelSpec{ + Name: "foo", + Query: "select * from foo", + }, + &kolide.LabelSpec{ + Name: "bar", + Query: "select * from bar", + }, + &kolide.LabelSpec{ + Name: "bing", + Query: "select * from bing", + }, + } + err := ds.ApplyLabelSpecs(labelSpecs) + require.Nil(t, err) + + specs := []*kolide.PackSpec{ + &kolide.PackSpec{ + ID: 1, + Name: "test_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + "foo", + "bar", + "bing", + }, + }, + }, + &kolide.PackSpec{ + ID: 2, + Name: "test 2", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + "bing", + }, + }, + }, + &kolide.PackSpec{ + ID: 3, + Name: "test 3", + }, + } + err = ds.ApplyPackSpecs(specs) + require.Nil(t, err) + + labels, err := ds.ListLabelsForPack(specs[0].ID) + require.Nil(t, err) + assert.Len(t, labels, 3) + + labels, err = ds.ListLabelsForPack(specs[1].ID) + require.Nil(t, err) + assert.Len(t, labels, 1) + assert.Equal(t, "bing", labels[0].Name) + + labels, err = ds.ListLabelsForPack(specs[2].ID) + require.Nil(t, err) + assert.Len(t, labels, 0) +} + +func testListPacksForHost(t *testing.T, ds kolide.Datastore) { + if ds.Name() == "inmem" { + t.Skip("inmem is deprecated") + } + + mockClock := clock.NewMockClock() + + l1 := &kolide.LabelSpec{ + ID: 1, + Name: "foo", + } + l2 := &kolide.LabelSpec{ + ID: 2, + Name: "bar", + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{l1, l2}) + require.Nil(t, err) + + p1 := &kolide.PackSpec{ + ID: 1, + Name: "foo_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + l1.Name, + l2.Name, + }, + }, + } + p2 := &kolide.PackSpec{ + ID: 2, + Name: "shmoo_pack", + Targets: kolide.PackSpecTargets{ + Labels: []string{ + l2.Name, + }, + }, + } + err = ds.ApplyPackSpecs([]*kolide.PackSpec{p1, p2}) + require.Nil(t, err) + + h1 := test.NewHost(t, ds, "h1.local", "10.10.10.1", "1", "1", mockClock.Now()) + + packs, err := ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + require.Len(t, packs, 0) + + err = ds.RecordLabelQueryExecutions( + h1, + map[uint]bool{l1.ID: true}, + mockClock.Now(), + ) + require.Nil(t, err) + + packs, err = ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + if assert.Len(t, packs, 1) { + assert.Equal(t, "foo_pack", packs[0].Name) + } + + err = ds.RecordLabelQueryExecutions( + h1, + map[uint]bool{l1.ID: false, l2.ID: true}, + mockClock.Now(), + ) + require.Nil(t, err) + + packs, err = ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + assert.Len(t, packs, 2) + + err = ds.RecordLabelQueryExecutions( + h1, + map[uint]bool{l1.ID: true, l2.ID: true}, + mockClock.Now(), + ) + require.Nil(t, err) + + packs, err = ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + assert.Len(t, packs, 2) + + h2 := test.NewHost(t, ds, "h2.local", "10.10.10.2", "2", "2", mockClock.Now()) + + err = ds.RecordLabelQueryExecutions( + h2, + map[uint]bool{l2.ID: true}, + mockClock.Now(), + ) + require.Nil(t, err) + + packs, err = ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + assert.Len(t, packs, 2) + + err = ds.RecordLabelQueryExecutions( + h1, + map[uint]bool{l2.ID: false}, + mockClock.Now(), + ) + require.Nil(t, err) + + packs, err = ds.ListPacksForHost(h1.ID) + require.Nil(t, err) + if assert.Len(t, packs, 1) { + assert.Equal(t, "foo_pack", packs[0].Name) + } } diff --git a/server/datastore/datastore_queries_test.go b/server/datastore/datastore_queries_test.go index 5f9eca791..64dcd289b 100644 --- a/server/datastore/datastore_queries_test.go +++ b/server/datastore/datastore_queries_test.go @@ -2,15 +2,76 @@ package datastore import ( "fmt" + "sort" "testing" "github.com/kolide/fleet/server/kolide" "github.com/kolide/fleet/server/test" - "github.com/patrickmn/sortutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func testApplyQueries(t *testing.T, ds kolide.Datastore) { + zwass := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) + groob := test.NewUser(t, ds, "Victor", "groob", "victor@kolide.co", true) + expectedQueries := []*kolide.Query{ + {Name: "foo", Description: "get the foos", Query: "select * from foo"}, + {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, + } + + // Zach creates some queries + err := ds.ApplyQueries(zwass.ID, expectedQueries) + require.Nil(t, err) + + queries, err := ds.ListQueries(kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, queries, len(expectedQueries)) + for i, q := range queries { + comp := expectedQueries[i] + assert.Equal(t, comp.Name, q.Name) + assert.Equal(t, comp.Description, q.Description) + assert.Equal(t, comp.Query, q.Query) + assert.Equal(t, zwass.ID, q.AuthorID) + } + + // Victor modifies a query (but also pushes the same version of the + // first query) + expectedQueries[1].Query = "not really a valid query ;)" + err = ds.ApplyQueries(groob.ID, expectedQueries) + require.Nil(t, err) + + queries, err = ds.ListQueries(kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, queries, len(expectedQueries)) + for i, q := range queries { + comp := expectedQueries[i] + assert.Equal(t, comp.Name, q.Name) + assert.Equal(t, comp.Description, q.Description) + assert.Equal(t, comp.Query, q.Query) + assert.Equal(t, groob.ID, q.AuthorID) + } + + // Zach adds a third query (but does not re-apply the others) + expectedQueries = append(expectedQueries, + &kolide.Query{Name: "trouble", Description: "Look out!", Query: "select * from time"}, + ) + err = ds.ApplyQueries(zwass.ID, []*kolide.Query{expectedQueries[2]}) + require.Nil(t, err) + + queries, err = ds.ListQueries(kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, queries, len(expectedQueries)) + for i, q := range queries { + comp := expectedQueries[i] + assert.Equal(t, comp.Name, q.Name) + assert.Equal(t, comp.Description, q.Description) + assert.Equal(t, comp.Query, q.Query) + } + assert.Equal(t, groob.ID, queries[0].AuthorID) + assert.Equal(t, groob.ID, queries[1].AuthorID) + assert.Equal(t, zwass.ID, queries[2].AuthorID) +} + func testDeleteQuery(t *testing.T, ds kolide.Datastore) { user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) @@ -24,7 +85,7 @@ func testDeleteQuery(t *testing.T, ds kolide.Datastore) { require.NotNil(t, query) assert.NotEqual(t, query.ID, 0) - err = ds.DeleteQuery(query.ID) + err = ds.DeleteQuery(query.Name) require.Nil(t, err) assert.NotEqual(t, query.ID, 0) @@ -35,15 +96,14 @@ func testDeleteQuery(t *testing.T, ds kolide.Datastore) { func testGetQueryByName(t *testing.T, ds kolide.Datastore) { user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) test.NewQuery(t, ds, "q1", "select * from time", user.ID, true) - actual, ok, err := ds.QueryByName("q1") + actual, err := ds.QueryByName("q1") require.Nil(t, err) - assert.True(t, ok) assert.Equal(t, "q1", actual.Name) assert.Equal(t, "select * from time", actual.Query) - actual, ok, err = ds.QueryByName("xxx") + actual, err = ds.QueryByName("xxx") assert.Nil(t, err) - assert.False(t, ok) + assert.True(t, kolide.IsNotFound(err)) } func testDeleteQueries(t *testing.T, ds kolide.Datastore) { @@ -137,51 +197,125 @@ func testListQuery(t *testing.T, ds kolide.Datastore) { assert.Equal(t, 10, len(results)) } -func checkPacks(t *testing.T, expected []kolide.Pack, actual []kolide.Pack) { - sortutil.AscByField(expected, "ID") - sortutil.AscByField(actual, "ID") - assert.Equal(t, expected, actual) -} - func testLoadPacksForQueries(t *testing.T, ds kolide.Datastore) { - user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) - - q1 := test.NewQuery(t, ds, "q1", "select * from time", user.ID, true) - q2 := test.NewQuery(t, ds, "q2", "select * from osquery_info", user.ID, true) - - p1 := test.NewPack(t, ds, "p1") - p2 := test.NewPack(t, ds, "p2") - p3 := test.NewPack(t, ds, "p3") - - var err error - - test.NewScheduledQuery(t, ds, p2.ID, q1.ID, 60, false, false) - - q1, err = ds.Query(q1.ID) + zwass := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) + queries := []*kolide.Query{ + {Name: "q1", Query: "select * from time"}, + {Name: "q2", Query: "select * from osquery_info"}, + } + err := ds.ApplyQueries(zwass.ID, queries) require.Nil(t, err) - q2, err = ds.Query(q2.ID) - require.Nil(t, err) - checkPacks(t, []kolide.Pack{*p2}, q1.Packs) - checkPacks(t, []kolide.Pack{}, q2.Packs) - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, false, false) - test.NewScheduledQuery(t, ds, p3.ID, q2.ID, 60, false, false) + specs := []*kolide.PackSpec{ + &kolide.PackSpec{Name: "p1"}, + &kolide.PackSpec{Name: "p2"}, + &kolide.PackSpec{Name: "p3"}, + } + err = ds.ApplyPackSpecs(specs) + require.Nil(t, err) - q1, err = ds.Query(q1.ID) + q0, err := ds.QueryByName(queries[0].Name) require.Nil(t, err) - q2, err = ds.Query(q2.ID) - require.Nil(t, err) - checkPacks(t, []kolide.Pack{*p2}, q1.Packs) - checkPacks(t, []kolide.Pack{*p1, *p3}, q2.Packs) + assert.Empty(t, q0.Packs) - test.NewScheduledQuery(t, ds, p3.ID, q1.ID, 60, false, false) + q1, err := ds.QueryByName(queries[1].Name) + require.Nil(t, err) + assert.Empty(t, q1.Packs) - q1, err = ds.Query(q1.ID) + specs = []*kolide.PackSpec{ + &kolide.PackSpec{ + Name: "p2", + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Interval: 60, + }, + }, + }, + } + err = ds.ApplyPackSpecs(specs) require.Nil(t, err) - q2, err = ds.Query(q2.ID) + + q0, err = ds.QueryByName(queries[0].Name) require.Nil(t, err) - checkPacks(t, []kolide.Pack{*p2, *p3}, q1.Packs) - checkPacks(t, []kolide.Pack{*p1, *p3}, q2.Packs) + if assert.Len(t, q0.Packs, 1) { + assert.Equal(t, "p2", q0.Packs[0].Name) + } + + q1, err = ds.QueryByName(queries[1].Name) + require.Nil(t, err) + assert.Empty(t, q1.Packs) + + specs = []*kolide.PackSpec{ + &kolide.PackSpec{ + Name: "p1", + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Interval: 60, + }, + }, + }, + &kolide.PackSpec{ + Name: "p3", + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Interval: 60, + }, + }, + }, + } + err = ds.ApplyPackSpecs(specs) + require.Nil(t, err) + + q0, err = ds.QueryByName(queries[0].Name) + require.Nil(t, err) + if assert.Len(t, q0.Packs, 1) { + assert.Equal(t, "p2", q0.Packs[0].Name) + } + + q1, err = ds.QueryByName(queries[1].Name) + require.Nil(t, err) + if assert.Len(t, q1.Packs, 2) { + sort.Slice(q1.Packs, func(i, j int) bool { return q1.Packs[i].Name < q1.Packs[j].Name }) + assert.Equal(t, "p1", q1.Packs[0].Name) + assert.Equal(t, "p3", q1.Packs[1].Name) + } + + specs = []*kolide.PackSpec{ + &kolide.PackSpec{ + Name: "p3", + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Interval: 60, + }, + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Interval: 60, + }, + }, + }, + } + err = ds.ApplyPackSpecs(specs) + require.Nil(t, err) + + q0, err = ds.QueryByName(queries[0].Name) + require.Nil(t, err) + if assert.Len(t, q0.Packs, 2) { + sort.Slice(q0.Packs, func(i, j int) bool { return q0.Packs[i].Name < q0.Packs[j].Name }) + assert.Equal(t, "p2", q0.Packs[0].Name) + assert.Equal(t, "p3", q0.Packs[1].Name) + } + + q1, err = ds.QueryByName(queries[1].Name) + require.Nil(t, err) + if assert.Len(t, q1.Packs, 2) { + sort.Slice(q1.Packs, func(i, j int) bool { return q1.Packs[i].Name < q1.Packs[j].Name }) + assert.Equal(t, "p1", q1.Packs[0].Name) + assert.Equal(t, "p3", q1.Packs[1].Name) + } } func testDuplicateNewQuery(t *testing.T, ds kolide.Datastore) { diff --git a/server/datastore/datastore_scheduled_queries_test.go b/server/datastore/datastore_scheduled_queries_test.go index ea9f6e98c..d29221dd8 100644 --- a/server/datastore/datastore_scheduled_queries_test.go +++ b/server/datastore/datastore_scheduled_queries_test.go @@ -9,113 +9,69 @@ import ( "github.com/stretchr/testify/require" ) -func testNewScheduledQuery(t *testing.T, ds kolide.Datastore) { - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - - query, err := ds.NewScheduledQuery(&kolide.ScheduledQuery{ - PackID: p1.ID, - QueryID: q1.ID, - }) - require.Nil(t, err) - assert.Equal(t, "foo", query.Name) - assert.Equal(t, "select * from time;", query.Query) -} - -func testScheduledQuery(t *testing.T, ds kolide.Datastore) { - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := ds.ScheduledQuery(sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) -} - -func testDeleteScheduledQuery(t *testing.T, ds kolide.Datastore) { - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := ds.ScheduledQuery(sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) - - err = ds.DeleteScheduledQuery(sq1.ID) - require.Nil(t, err) - - _, err = ds.ScheduledQuery(sq1.ID) - require.NotNil(t, err) -} - func testListScheduledQueriesInPack(t *testing.T, ds kolide.Datastore) { - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - q2 := test.NewQuery(t, ds, "bar", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - - test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - queries, err := ds.ListScheduledQueriesInPack(p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, queries, 1) - assert.Equal(t, uint(60), queries[0].Interval) - - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, false, false) - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, true, false) - - queries, err = ds.ListScheduledQueriesInPack(p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, queries, 3) -} - -func testSaveScheduledQuery(t *testing.T, ds kolide.Datastore) { - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := ds.ScheduledQuery(sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) - - query.Interval = uint(120) - query, err = ds.SaveScheduledQuery(query) - require.Nil(t, err) - assert.Equal(t, uint(120), query.Interval) - - queryVerify, err := ds.ScheduledQuery(sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(120), queryVerify.Interval) -} - -func testScheduledQueryWithDeletedPack(t *testing.T, ds kolide.Datastore) { - // When a pack is soft-deleted, it should not appear in the list of - // packs associated with a query. - - if ds.Name() == "inmem" { - t.Skip("inmem is being deprecated, test skipped") + zwass := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) + queries := []*kolide.Query{ + {Name: "foo", Description: "get the foos", Query: "select * from foo"}, + {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, } - - user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true) - query := test.NewQuery(t, ds, "q1", "select 1", user.ID, true) - pack := test.NewPack(t, ds, "foobar_pack") - test.NewScheduledQuery(t, ds, pack.ID, query.ID, 60, false, false) - - actual, err := ds.Query(query.ID) + err := ds.ApplyQueries(zwass.ID, queries) require.Nil(t, err) - assert.Equal(t, "q1", actual.Name) - assert.Equal(t, "select 1", actual.Query) - assert.Equal(t, []kolide.Pack{*pack}, actual.Packs) - require.Nil(t, ds.DeletePack(pack.ID)) - - actual, err = ds.Query(query.ID) + specs := []*kolide.PackSpec{ + &kolide.PackSpec{ + Name: "baz", + Targets: kolide.PackSpecTargets{Labels: []string{}}, + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Description: "test_foo", + Interval: 60, + }, + }, + }, + } + err = ds.ApplyPackSpecs(specs) require.Nil(t, err) - assert.Equal(t, "q1", actual.Name) - assert.Equal(t, "select 1", actual.Query) - assert.Empty(t, actual.Packs) + + gotQueries, err := ds.ListScheduledQueriesInPack(1, kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, gotQueries, 1) + assert.Equal(t, uint(60), gotQueries[0].Interval) + assert.Equal(t, "test_foo", gotQueries[0].Description) + assert.Equal(t, "select * from foo", gotQueries[0].Query) + + boolPtr := func(b bool) *bool { return &b } + specs = []*kolide.PackSpec{ + &kolide.PackSpec{ + Name: "baz", + Targets: kolide.PackSpecTargets{Labels: []string{}}, + Queries: []kolide.PackSpecQuery{ + kolide.PackSpecQuery{ + QueryName: queries[0].Name, + Description: "test_foo", + Interval: 60, + }, + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Name: "test bar", + Description: "test_bar", + Interval: 60, + }, + kolide.PackSpecQuery{ + QueryName: queries[1].Name, + Name: "test bar snapshot", + Description: "test_bar", + Interval: 60, + Snapshot: boolPtr(true), + }, + }, + }, + } + err = ds.ApplyPackSpecs(specs) + require.Nil(t, err) + + gotQueries, err = ds.ListScheduledQueriesInPack(1, kolide.ListOptions{}) + require.Nil(t, err) + require.Len(t, gotQueries, 3) } diff --git a/server/datastore/datastore_targets_test.go b/server/datastore/datastore_targets_test.go index 5c7a9860e..4f97b87ff 100644 --- a/server/datastore/datastore_targets_test.go +++ b/server/datastore/datastore_targets_test.go @@ -43,19 +43,18 @@ func testCountHostsInTargets(t *testing.T, ds kolide.Datastore) { const thirtyDaysAndAMinuteAgo = -1 * (30*24*60 + 1) h6 := initHost(mockClock.Now().Add(thirtyDaysAndAMinuteAgo*time.Minute), 3600, 3600) - l1, err := ds.NewLabel(&kolide.Label{ + l1 := kolide.LabelSpec{ + ID: 1, Name: "label foo", Query: "query foo", - }) - require.Nil(t, err) - require.NotZero(t, l1.ID) - - l2, err := ds.NewLabel(&kolide.Label{ + } + l2 := kolide.LabelSpec{ + ID: 2, Name: "label bar", - Query: "query foo", - }) + Query: "query bar", + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1, &l2}) require.Nil(t, err) - require.NotZero(t, l2.ID) for _, h := range []*kolide.Host{h1, h2, h3, h6} { err = ds.RecordLabelQueryExecutions(h, map[uint]bool{l1.ID: true}, mockClock.Now()) diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index aa8a547c8..885a2550e 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -47,22 +47,19 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testSaveHosts, testDeleteHost, testListHost, - testGetHostsInPack, + testListHostsInPack, + testListPacksForHost, + testHostIDsByName, + testListPacks, testDistributedQueryCampaign, testCleanupDistributedQueryCampaigns, testBuiltInLabels, testLoadPacksForQueries, - testScheduledQuery, - testDeleteScheduledQuery, testListScheduledQueriesInPack, - testSaveScheduledQuery, testOptions, - testNewScheduledQuery, testOptionsToConfig, testGetPackByName, testGetQueryByName, - testScheduledQueryWithDeletedPack, - testDecorators, testFileIntegrityMonitoring, testYARAStore, testAddLabelToPackTwice, @@ -71,12 +68,22 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testDuplicateNewQuery, testIdempotentDeleteHost, testChangeEmail, - testSaveLabel, + testChangeLabelDetails, testFlappingNetworkInterfaces, - testReplaceDeletedLabel, testMigrationStatus, testUnicode, testCountHostsInTargets, testHostStatus, testResetOptions, + testApplyOsqueryOptions, + testApplyOsqueryOptionsNoOverrides, + testOsqueryOptionsForHost, + testApplyQueries, + testApplyPackSpecRoundtrip, + testApplyPackSpecMissingQueries, + testGetPackSpec, + testApplyLabelSpecsRoundtrip, + testGetLabelSpec, + testLabelIDsByName, + testListLabelsForPack, } diff --git a/server/datastore/datastore_unicode_test.go b/server/datastore/datastore_unicode_test.go index 5b9351cda..905df448b 100644 --- a/server/datastore/datastore_unicode_test.go +++ b/server/datastore/datastore_unicode_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/kolide/fleet/server/kolide" + "github.com/kolide/fleet/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,10 +15,15 @@ func testUnicode(t *testing.T, ds kolide.Datastore) { t.Skip("inmem is being deprecated, test skipped") } - label, err := ds.NewLabel(&kolide.Label{Name: "測試"}) + l1 := kolide.LabelSpec{ + ID: 1, + Name: "測試", + Query: "query foo", + } + err := ds.ApplyLabelSpecs([]*kolide.LabelSpec{&l1}) require.Nil(t, err) - label, err = ds.Label(label.ID) + label, err := ds.Label(l1.ID) require.Nil(t, err) assert.Equal(t, "測試", label.Name) @@ -35,10 +41,10 @@ func testUnicode(t *testing.T, ds kolide.Datastore) { require.Nil(t, err) user, err = ds.User(user.Username) + require.Nil(t, err) assert.Equal(t, "🍱", user.Username) - pack, err := ds.NewPack(&kolide.Pack{Name: "👨🏾‍🚒"}) - require.Nil(t, err) + pack := test.NewPack(t, ds, "👨🏾‍🚒") pack, err = ds.Pack(pack.ID) assert.Equal(t, "👨🏾‍🚒", pack.Name) diff --git a/server/datastore/inmem/inmem.go b/server/datastore/inmem/inmem.go index def81f0d4..628844b57 100644 --- a/server/datastore/inmem/inmem.go +++ b/server/datastore/inmem/inmem.go @@ -39,7 +39,10 @@ type Datastore struct { yaraSignatureGroups map[uint]*kolide.YARASignatureGroup appConfig *kolide.AppConfig config *config.KolideConfig - kolide.TargetStore + + // Embedded interface to avoid implementing new methods for (now + // deprecated) inmem. + kolide.Datastore } func New(config config.KolideConfig) (*Datastore, error) { @@ -131,7 +134,7 @@ func (d *Datastore) MigrateData() error { SMTPVerifySSLCerts: true, } - return d.createBuiltinLabels() + return nil } func (m *Datastore) MigrationStatus() (kolide.MigrationStatus, error) { @@ -155,10 +158,6 @@ func (d *Datastore) Initialize() error { return err } - if err := d.createDevLabels(); err != nil { - return err - } - if err := d.createDevOrgInfo(); err != nil { return err } @@ -242,46 +241,9 @@ func (d *Datastore) createDevPacksAndQueries() error { return err } - _, err = d.NewScheduledQuery(&kolide.ScheduledQuery{ - QueryID: query1.ID, - PackID: pack1.ID, - Interval: 60, - }) - if err != nil { - return err - } - - t := true - _, err = d.NewScheduledQuery(&kolide.ScheduledQuery{ - QueryID: query3.ID, - PackID: pack1.ID, - Interval: 60, - Snapshot: &t, - }) - if err != nil { - return err - } - - _, err = d.NewScheduledQuery(&kolide.ScheduledQuery{ - QueryID: query2.ID, - PackID: pack2.ID, - Interval: 60, - }) return err } -func (d *Datastore) createBuiltinLabels() error { - for _, label := range appstate.Labels2() { - label := label - _, err := d.NewLabel(&label) - if err != nil { - return err - } - } - - return nil -} - // Bootstrap a few users when using the in-memory database. // Each user's default password will just be their username. func (d *Datastore) createDevUsers() error { @@ -483,43 +445,3 @@ func (d *Datastore) createDevOrgInfo() error { _, err := d.NewAppConfig(devOrgInfo) return err } - -func (d *Datastore) createDevLabels() error { - labels := []kolide.Label{ - { - UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ - CreateTimestamp: kolide.CreateTimestamp{ - CreatedAt: time.Date(2016, time.October, 27, 8, 31, 16, 0, time.UTC), - }, - UpdateTimestamp: kolide.UpdateTimestamp{ - UpdatedAt: time.Date(2016, time.October, 27, 8, 31, 16, 0, time.UTC), - }, - }, - Name: "dev_label_apache", - Query: "select * from processes where name like '%Apache%'", - }, - { - UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{ - CreateTimestamp: kolide.CreateTimestamp{ - CreatedAt: time.Now().Add(-1 * time.Hour), - }, - UpdateTimestamp: kolide.UpdateTimestamp{ - UpdatedAt: time.Now(), - }, - }, - - Name: "dev_label_darwin", - Query: "select * from osquery_info where platform='darwin'", - }, - } - - for _, label := range labels { - label := label - _, err := d.NewLabel(&label) - if err != nil { - return err - } - } - - return nil -} diff --git a/server/datastore/inmem/labels.go b/server/datastore/inmem/labels.go index 5cd6c200e..7093cbf45 100644 --- a/server/datastore/inmem/labels.go +++ b/server/datastore/inmem/labels.go @@ -104,14 +104,6 @@ func (d *Datastore) RecordLabelQueryExecutions(host *kolide.Host, results map[ui return nil } -func (d *Datastore) DeleteLabel(lid uint) error { - d.mtx.Lock() - delete(d.labels, lid) - d.mtx.Unlock() - - return nil -} - func (d *Datastore) Label(lid uint) (*kolide.Label, error) { d.mtx.Lock() label, ok := d.labels[lid] diff --git a/server/datastore/inmem/packs.go b/server/datastore/inmem/packs.go index b2480bd36..6b1a255de 100644 --- a/server/datastore/inmem/packs.go +++ b/server/datastore/inmem/packs.go @@ -49,19 +49,6 @@ func (d *Datastore) SavePack(pack *kolide.Pack) error { return nil } -func (d *Datastore) DeletePack(pid uint) error { - d.mtx.Lock() - defer d.mtx.Unlock() - - if _, ok := d.packs[pid]; !ok { - return notFound("Pack").WithID(pid) - } - - delete(d.packs, pid) - - return nil -} - func (d *Datastore) Pack(id uint) (*kolide.Pack, error) { d.mtx.Lock() defer d.mtx.Unlock() diff --git a/server/datastore/inmem/queries.go b/server/datastore/inmem/queries.go index 3c4805e08..336d7d352 100644 --- a/server/datastore/inmem/queries.go +++ b/server/datastore/inmem/queries.go @@ -26,17 +26,6 @@ func (d *Datastore) NewQuery(query *kolide.Query, opts ...kolide.OptionalArg) (* return &newQuery, nil } -func (d *Datastore) QueryByName(name string, opts ...kolide.OptionalArg) (*kolide.Query, bool, error) { - d.mtx.Lock() - defer d.mtx.Unlock() - for _, q := range d.queries { - if name == q.Name { - return q, true, nil - } - } - return nil, false, nil -} - func (d *Datastore) SaveQuery(query *kolide.Query) error { d.mtx.Lock() defer d.mtx.Unlock() @@ -49,18 +38,6 @@ func (d *Datastore) SaveQuery(query *kolide.Query) error { return nil } -func (d *Datastore) DeleteQuery(qid uint) error { - d.mtx.Lock() - defer d.mtx.Unlock() - - if _, ok := d.queries[qid]; !ok { - return notFound("Query").WithID(qid) - } - - delete(d.queries, qid) - return nil -} - // DeleteQueries (soft) deletes the existing query objects with the provided // IDs. The number of deleted queries is returned along with any error. func (d *Datastore) DeleteQueries(ids []uint) (uint, error) { @@ -158,7 +135,7 @@ func (d *Datastore) loadPacksForQueries(queries []*kolide.Query) error { for _, q := range queries { q.Packs = make([]kolide.Pack, 0) for _, sq := range d.scheduledQueries { - if sq.QueryID == q.ID { + if sq.QueryName == q.Name { q.Packs = append(q.Packs, *d.packs[sq.PackID]) } } diff --git a/server/datastore/inmem/scheduled_queries.go b/server/datastore/inmem/scheduled_queries.go deleted file mode 100644 index 89f0b9c8c..000000000 --- a/server/datastore/inmem/scheduled_queries.go +++ /dev/null @@ -1,113 +0,0 @@ -package inmem - -import ( - "sort" - - "github.com/kolide/fleet/server/kolide" -) - -func (d *Datastore) NewScheduledQuery(sq *kolide.ScheduledQuery, opts ...kolide.OptionalArg) (*kolide.ScheduledQuery, error) { - d.mtx.Lock() - defer d.mtx.Unlock() - - newScheduledQuery := *sq - - newScheduledQuery.ID = d.nextID(newScheduledQuery) - d.scheduledQueries[newScheduledQuery.ID] = &newScheduledQuery - - newScheduledQuery.Query = d.queries[newScheduledQuery.QueryID].Query - newScheduledQuery.Name = d.queries[newScheduledQuery.QueryID].Name - - return &newScheduledQuery, nil -} - -func (d *Datastore) SaveScheduledQuery(sq *kolide.ScheduledQuery) (*kolide.ScheduledQuery, error) { - d.mtx.Lock() - defer d.mtx.Unlock() - - if _, ok := d.scheduledQueries[sq.ID]; !ok { - return nil, notFound("ScheduledQuery").WithID(sq.ID) - } - - d.scheduledQueries[sq.ID] = sq - return sq, nil -} - -func (d *Datastore) DeleteScheduledQuery(id uint) error { - d.mtx.Lock() - defer d.mtx.Unlock() - - if _, ok := d.scheduledQueries[id]; !ok { - return notFound("ScheduledQuery").WithID(id) - } - - delete(d.scheduledQueries, id) - return nil -} - -func (d *Datastore) ScheduledQuery(id uint) (*kolide.ScheduledQuery, error) { - d.mtx.Lock() - defer d.mtx.Unlock() - - sq, ok := d.scheduledQueries[id] - if !ok { - return nil, notFound("ScheduledQuery").WithID(id) - } - - sq.Name = d.queries[sq.QueryID].Name - sq.Query = d.queries[sq.QueryID].Query - - return sq, nil -} - -func (d *Datastore) ListScheduledQueriesInPack(id uint, opt kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { - d.mtx.Lock() - defer d.mtx.Unlock() - - // We need to sort by keys to provide reliable ordering - keys := []int{} - for k, sq := range d.scheduledQueries { - if sq.PackID == id { - keys = append(keys, int(k)) - } - } - - if len(keys) == 0 { - return []*kolide.ScheduledQuery{}, nil - } - - sort.Ints(keys) - - scheduledQueries := []*kolide.ScheduledQuery{} - for _, k := range keys { - q := d.scheduledQueries[uint(k)] - q.Name = d.queries[q.QueryID].Name - q.Query = d.queries[q.QueryID].Query - scheduledQueries = append(scheduledQueries, q) - } - - // Apply ordering - if opt.OrderKey != "" { - var fields = map[string]string{ - "id": "ID", - "created_at": "CreatedAt", - "updated_at": "UpdatedAt", - "name": "Name", - "query": "Query", - "interval": "Interval", - "snapshot": "Snapshot", - "removed": "Removed", - "platform": "Platform", - "version": "Version", - } - if err := sortResults(scheduledQueries, opt, fields); err != nil { - return nil, err - } - } - - // Apply limit/offset - low, high := d.getLimitOffsetSliceBounds(opt, len(scheduledQueries)) - scheduledQueries = scheduledQueries[low:high] - - return scheduledQueries, nil -} diff --git a/server/datastore/mysql/decorators.go b/server/datastore/mysql/decorators.go deleted file mode 100644 index 455b59b07..000000000 --- a/server/datastore/mysql/decorators.go +++ /dev/null @@ -1,99 +0,0 @@ -package mysql - -import ( - "database/sql" - - "github.com/pkg/errors" - - "github.com/kolide/fleet/server/kolide" -) - -func (ds *Datastore) SaveDecorator(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - db := ds.getTransaction(opts) - sqlStatement := - "UPDATE decorators SET " + - "`name` = ?, " + - "`query` = ?, " + - "`type` = ?, " + - "`interval` = ? " + - "WHERE id = ?" - _, err := db.Exec( - sqlStatement, - dec.Name, - dec.Query, - dec.Type, - dec.Interval, - dec.ID, - ) - if err != nil { - return errors.Wrap(err, "saving decorator") - } - return nil -} - -func (ds *Datastore) NewDecorator(decorator *kolide.Decorator, opts ...kolide.OptionalArg) (*kolide.Decorator, error) { - db := ds.getTransaction(opts) - sqlStatement := - "INSERT INTO decorators (" + - "`name`," + - "`query`," + - "`type`," + - "`interval` ) " + - "VALUES (?, ?, ?, ?)" - result, err := db.Exec(sqlStatement, decorator.Name, decorator.Query, decorator.Type, decorator.Interval) - - if err != nil { - return nil, errors.Wrap(err, "creating decorator") - } - id, _ := result.LastInsertId() - decorator.ID = uint(id) - return decorator, nil -} - -func (ds *Datastore) DeleteDecorator(id uint) error { - sqlStatement := ` - DELETE FROM decorators - WHERE id = ? - ` - res, err := ds.db.Exec(sqlStatement, id) - if err != nil { - return errors.Wrap(err, "deleting decorator") - } - deleted, _ := res.RowsAffected() - if deleted < 1 { - return notFound("Decorator").WithID(id) - } - return nil -} - -func (ds *Datastore) Decorator(id uint) (*kolide.Decorator, error) { - sqlStatement := ` - SELECT * - FROM decorators - WHERE id = ? - ` - var result kolide.Decorator - err := ds.db.Get(&result, sqlStatement, id) - if err != nil { - if err == sql.ErrNoRows { - return nil, notFound("Decorator").WithID(id) - } - return nil, errors.Wrap(err, "retrieving decorator") - } - return &result, nil -} - -func (ds *Datastore) ListDecorators(opts ...kolide.OptionalArg) ([]*kolide.Decorator, error) { - db := ds.getTransaction(opts) - sqlStatement := ` - SELECT * - FROM decorators - ORDER by built_in DESC, name ASC - ` - var results []*kolide.Decorator - err := db.Select(&results, sqlStatement) - if err != nil { - return nil, errors.Wrap(err, "listing decorators") - } - return results, nil -} diff --git a/server/datastore/mysql/delete.go b/server/datastore/mysql/delete.go index 06d7ca535..f85b7bc8f 100644 --- a/server/datastore/mysql/delete.go +++ b/server/datastore/mysql/delete.go @@ -10,7 +10,7 @@ func (d *Datastore) deleteEntity(dbTable string, id uint) error { deleteStmt := fmt.Sprintf( ` UPDATE %s SET deleted_at = ?, deleted = TRUE - WHERE id = ? AND NOT deleted + WHERE id = ? AND NOT deleted `, dbTable) result, err := d.db.Exec(deleteStmt, d.clock.Now(), id) if err != nil { @@ -22,3 +22,19 @@ func (d *Datastore) deleteEntity(dbTable string, id uint) error { } return nil } + +// deleteEntityByName hard deletes an entity with the given name from the given +// DB table, returning a notFound error if appropriate. +// Note: deleteEntity uses soft deletion, but we are moving off this pattern. +func (d *Datastore) deleteEntityByName(dbTable string, name string) error { + deleteStmt := fmt.Sprintf("DELETE FROM %s WHERE name = ?", dbTable) + result, err := d.db.Exec(deleteStmt, name) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("delete %s", dbTable)) + } + rows, _ := result.RowsAffected() + if rows != 1 { + return notFound(dbTable).WithName(name) + } + return nil +} diff --git a/server/datastore/mysql/errors.go b/server/datastore/mysql/errors.go index 85b680d1d..f0ae09c62 100644 --- a/server/datastore/mysql/errors.go +++ b/server/datastore/mysql/errors.go @@ -9,6 +9,7 @@ import ( type notFoundError struct { ID uint + Name string Message string ResourceType string } @@ -23,6 +24,9 @@ func (e *notFoundError) Error() string { if e.ID != 0 { return fmt.Sprintf("%s %d was not found in the datastore", e.ResourceType, e.ID) } + if e.Name != "" { + return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Name) + } if e.Message != "" { return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Message) } @@ -34,6 +38,11 @@ func (e *notFoundError) WithID(id uint) error { return e } +func (e *notFoundError) WithName(name string) error { + e.Name = name + return e +} + func (e *notFoundError) WithMessage(msg string) error { e.Message = msg return e diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index fdcad79cc..07fe8fd0c 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -675,3 +675,23 @@ func (d *Datastore) DistributedQueriesForHost(host *kolide.Host) (map[uint]strin return results, nil } + +func (d *Datastore) HostIDsByName(hostnames []string) ([]uint, error) { + sqlStatement := ` + SELECT id FROM hosts + WHERE host_name IN (?) + ` + + sql, args, err := sqlx.In(sqlStatement, hostnames) + if err != nil { + return nil, errors.Wrap(err, "building query to get host IDs") + } + + var hostIDs []uint + if err := d.db.Select(&hostIDs, sql, args...); err != nil { + return nil, errors.Wrap(err, "get host IDs") + } + + return hostIDs, nil + +} diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 7f39f7923..97e1d0580 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -2,6 +2,7 @@ package mysql import ( "database/sql" + "fmt" "time" "github.com/jmoiron/sqlx" @@ -9,53 +10,94 @@ import ( "github.com/pkg/errors" ) -// NewLabel creates a new kolide.Label -func (d *Datastore) NewLabel(label *kolide.Label, opts ...kolide.OptionalArg) (*kolide.Label, error) { - db := d.getTransaction(opts) - var ( - deletedLabel kolide.Label - query string - ) - err := db.Get(&deletedLabel, - "SELECT * FROM labels WHERE name = ? AND deleted", label.Name) - switch err { - case nil: - query = ` - REPLACE INTO labels ( - name, - description, - query, - platform, - label_type - ) VALUES ( ?, ?, ?, ?, ?) - ` - case sql.ErrNoRows: - query = ` +func (d *Datastore) ApplyLabelSpecs(specs []*kolide.LabelSpec) (err error) { + tx, err := d.db.Beginx() + if err != nil { + return errors.Wrap(err, "begin ApplyLabelSpecs transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + sql := ` INSERT INTO labels ( name, description, query, platform, label_type - ) VALUES ( ?, ?, ?, ?, ?) + ) VALUES ( ?, ?, ?, ?, ? ) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + description = VALUES(description), + query = VALUES(query), + platform = VALUES(platform), + label_type = VALUES(label_type), + deleted = false ` - default: - return nil, errors.Wrap(err, "check for existing label") - } - result, err := db.Exec(query, label.Name, label.Description, label.Query, label.Platform, label.LabelType) + stmt, err := tx.Prepare(sql) if err != nil { - return nil, errors.Wrap(err, "inserting label") + return errors.Wrap(err, "prepare ApplyLabelSpecs insert") } - id, _ := result.LastInsertId() - label.ID = uint(id) - return label, nil + for _, s := range specs { + if s.Name == "" { + return errors.New("label name must not be empty") + } + _, err := stmt.Exec(s.Name, s.Description, s.Query, s.Platform, s.LabelType) + if err != nil { + return errors.Wrap(err, "exec ApplyLabelSpecs insert") + } + } + err = tx.Commit() + return errors.Wrap(err, "commit ApplyLabelSpecs transaction") } -// DeleteLabel soft deletes a kolide.Label -func (d *Datastore) DeleteLabel(lid uint) error { - return d.deleteEntity("labels", lid) +func (d *Datastore) GetLabelSpecs() ([]*kolide.LabelSpec, error) { + var specs []*kolide.LabelSpec + // Get basic specs + query := "SELECT name, description, query, platform, label_type FROM labels" + if err := d.db.Select(&specs, query); err != nil { + return nil, errors.Wrap(err, "get labels") + } + + return specs, nil +} + +func (d *Datastore) GetLabelSpec(name string) (*kolide.LabelSpec, error) { + var specs []*kolide.LabelSpec + query := ` +SELECT name, description, query, platform, label_type +FROM labels +WHERE name = ? +` + if err := d.db.Select(&specs, query, name); err != nil { + return nil, errors.Wrap(err, "get label") + } + if len(specs) == 0 { + return nil, notFound("Label").WithName(name) + } + if len(specs) > 1 { + return nil, errors.Errorf("expected 1 label row, got %d", len(specs)) + } + + return specs[0], nil +} + +// DeleteLabel deletes a kolide.Label +func (d *Datastore) DeleteLabel(name string) error { + return d.deleteEntityByName("labels", name) } // Label returns a kolide.Label identified by lid if one exists @@ -379,16 +421,22 @@ func (d *Datastore) SearchLabels(query string, omit ...uint) ([]kolide.Label, er return matches, nil } -func (d *Datastore) SaveLabel(label *kolide.Label) (*kolide.Label, error) { - query := ` - UPDATE labels SET - name = ?, - description = ? - WHERE id = ? +func (d *Datastore) LabelIDsByName(labels []string) ([]uint, error) { + sqlStatement := ` + SELECT id FROM labels + WHERE name IN (?) ` - _, err := d.db.Exec(query, label.Name, label.Description, label.ID) + + sql, args, err := sqlx.In(sqlStatement, labels) if err != nil { - return nil, errors.Wrap(err, "saving label") + return nil, errors.Wrap(err, "building query to get host IDs") } - return label, nil + + var hostIDs []uint + if err := d.db.Select(&hostIDs, sql, args...); err != nil { + return nil, errors.Wrap(err, "get host IDs") + } + + return hostIDs, nil + } diff --git a/server/datastore/mysql/migrations/data/20171212182458_MigrateOsqueryOptions.go b/server/datastore/mysql/migrations/data/20171212182458_MigrateOsqueryOptions.go new file mode 100644 index 000000000..2030b28fb --- /dev/null +++ b/server/datastore/mysql/migrations/data/20171212182458_MigrateOsqueryOptions.go @@ -0,0 +1,123 @@ +package data + +import ( + "database/sql" + "encoding/json" + "fmt" + "strconv" + + "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx/reflectx" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20171212182458, Down_20171212182458) +} + +type configForExport struct { + Options map[string]interface{} `json:"options"` + FilePaths map[string][]string `json:"file_paths,omitempty"` + Decorators kolide.Decorators `json:"decorators"` +} + +func Up_20171212182458(tx *sql.Tx) error { + // Migrate pre fleetctl osquery options to the new osquery options + // formats. + txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)} + + // Get basic osquery options + query := ` + SELECT * + FROM options + WHERE value != "null" + ` + // Intentionally initialize empty instead of nil so that we generate a + // config with empty options rather than a null value. + var opts []kolide.Option + if err := txx.Select(&opts, query); err != nil && err != sql.ErrNoRows { + return errors.Wrap(err, "getting options") + } + optConfig := map[string]interface{}{} + for _, opt := range opts { + optConfig[opt.Name] = opt.GetValue() + } + + // Get FIM paths from fim table + query = ` + SELECT fim.section_name, mf.file + FROM file_integrity_monitorings AS fim + INNER JOIN file_integrity_monitoring_files AS mf + ON (fim.id = mf.file_integrity_monitoring_id) + ` + rows, err := txx.Query(query) + if err != nil && err != sql.ErrNoRows { + return errors.Wrap(err, "retrieving fim paths") + } + fimConfig := kolide.FIMSections{} + for rows.Next() { + var sectionName, fileName string + err = rows.Scan(§ionName, &fileName) + if err != nil { + return errors.Wrap(err, "retrieving path for fim section") + } + fimConfig[sectionName] = append(fimConfig[sectionName], fileName) + } + + query = ` + SELECT * + FROM decorators + ORDER by built_in DESC, name ASC + ` + var decorators []*kolide.Decorator + err = txx.Select(&decorators, query) + if err != nil { + return errors.Wrap(err, "retrieving decorators") + } + + decConfig := kolide.Decorators{ + Interval: make(map[string][]string), + } + for _, dec := range decorators { + switch dec.Type { + case kolide.DecoratorLoad: + decConfig.Load = append(decConfig.Load, dec.Query) + case kolide.DecoratorAlways: + decConfig.Always = append(decConfig.Always, dec.Query) + case kolide.DecoratorInterval: + key := strconv.Itoa(int(dec.Interval)) + decConfig.Interval[key] = append(decConfig.Interval[key], dec.Query) + default: + fmt.Printf("Unable to migrate decorator. Please migrate manually: '%s'\n", dec.Query) + } + } + + // Create config JSON + config := configForExport{ + Options: optConfig, + FilePaths: fimConfig, + Decorators: decConfig, + } + confJSON, err := json.Marshal(config) + if err != nil { + return errors.Wrap(err, "marshal config JSON") + } + + // Save config JSON + query = ` + INSERT INTO osquery_options ( + override_type, override_identifier, options + ) VALUES (?, ?, ?) + ` + if _, err = txx.Exec(query, kolide.OptionOverrideTypeDefault, "", string(confJSON)); err != nil { + return errors.Wrap(err, "saving converted options") + } + + return nil +} + +func Down_20171212182458(tx *sql.Tx) error { + // No down migration + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20171116163618_CreateTableOsqueryOptions.go b/server/datastore/mysql/migrations/tables/20171116163618_CreateTableOsqueryOptions.go new file mode 100644 index 000000000..947390893 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20171116163618_CreateTableOsqueryOptions.go @@ -0,0 +1,24 @@ +package tables + +import "database/sql" + +func init() { + MigrationClient.AddMigration(Up_20171116163618, Down_20171116163618) +} + +func Up_20171116163618(tx *sql.Tx) error { + sqlStatement := "CREATE TABLE `osquery_options` (" + + "`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT," + + "`override_type` INT(1) NOT NULL, " + + "`override_identifier` VARCHAR(255) NOT NULL DEFAULT ''," + + "`options` JSON NOT NULL," + + "PRIMARY KEY (`id`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;" + _, err := tx.Exec(sqlStatement) + return err +} + +func Down_20171116163618(tx *sql.Tx) error { + _, err := tx.Exec("DROP TABLE IF EXISTS `osquery_options`;") + return err +} diff --git a/server/datastore/mysql/migrations/tables/20171219164727_UpdateScheduledQueryKey.go b/server/datastore/mysql/migrations/tables/20171219164727_UpdateScheduledQueryKey.go new file mode 100644 index 000000000..88d4da7b8 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20171219164727_UpdateScheduledQueryKey.go @@ -0,0 +1,74 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20171219164727, Down_20171219164727) +} + +func Up_20171219164727(tx *sql.Tx) error { + // Add query name column + query := ` + ALTER TABLE scheduled_queries + ADD COLUMN query_name varchar(255) + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "adding query_name column") + } + + // Populate query name column via join with query ID + query = ` + UPDATE scheduled_queries + SET query_name = (SELECT name from queries where id = query_id) + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "populating query_name column") + } + + // Add not null constraint + query = ` + ALTER TABLE scheduled_queries + MODIFY query_name varchar(255) NOT NULL + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "adding not null") + } + + // Add foreign key constraint + query = ` + ALTER TABLE scheduled_queries + ADD FOREIGN KEY (query_name) REFERENCES queries (name) + ` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "adding foreign key to query_name column") + } + + // Add `name` column to scheduled_queries + + query = ` +ALTER TABLE scheduled_queries +ADD COLUMN name varchar(255), +ADD COLUMN description varchar(255) +` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "adding name to scheduled_queries") + } + + query = ` +ALTER TABLE packs +DROP COLUMN created_by +` + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "removing created_by from packs") + } + + return nil +} + +func Down_20171219164727(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 98cef6206..f245c4b34 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -285,3 +285,16 @@ func generateMysqlConnectionString(conf config.MysqlConfig) string { return dsn } + +// isForeignKeyError checks if the provided error is a MySQL child foreign key +// error (Error #1452) +func isChildForeignKeyError(err error) bool { + mysqlErr, ok := err.(*mysql.MySQLError) + if !ok { + return false + } + + // https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_no_referenced_row_2 + const ER_NO_REFERENCED_ROW_2 = 1452 + return mysqlErr.Number == ER_NO_REFERENCED_ROW_2 +} diff --git a/server/datastore/mysql/osquery_options.go b/server/datastore/mysql/osquery_options.go new file mode 100644 index 000000000..73826f1fa --- /dev/null +++ b/server/datastore/mysql/osquery_options.go @@ -0,0 +1,132 @@ +package mysql + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/go-kit/kit/log/level" + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +type optionsRow struct { + ID int `db:"id"` + OverrideType kolide.OptionOverrideType `db:"override_type"` + OverrideIdentifier string `db:"override_identifier"` + Options string `db:"options"` +} + +func (d *Datastore) ApplyOptions(spec *kolide.OptionsSpec) (err error) { + tx, err := d.db.Begin() + if err != nil { + return errors.Wrap(err, "begin ApplyOptions transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + // Clear all the existing options + _, err = tx.Exec("DELETE FROM osquery_options") + if err != nil { + return errors.Wrap(err, "delete existing options") + } + + // Save new options + sql := ` + INSERT INTO osquery_options ( + override_type, override_identifier, options + ) VALUES (?, ?, ?) + ` + + // Default options + _, err = tx.Exec(sql, kolide.OptionOverrideTypeDefault, "", string(spec.Config)) + if err != nil { + return errors.Wrap(err, "saving default config") + } + + // Platform overrides + for platform, opts := range spec.Overrides.Platforms { + _, err = tx.Exec(sql, kolide.OptionOverrideTypePlatform, platform, string(opts)) + if err != nil { + return errors.Wrapf(err, "saving %s platform config", platform) + } + + } + + // Success! + err = tx.Commit() + if err != nil { + return errors.Wrap(err, "commit ApplyOptions transaction") + } + + return nil +} + +func (d *Datastore) GetOptions() (*kolide.OptionsSpec, error) { + var rows []optionsRow + if err := d.db.Select(&rows, "SELECT * FROM osquery_options"); err != nil { + return nil, errors.Wrap(err, "selecting options") + } + + spec := &kolide.OptionsSpec{ + Overrides: kolide.OptionsOverrides{ + Platforms: make(map[string]json.RawMessage), + }, + } + for _, row := range rows { + switch row.OverrideType { + case kolide.OptionOverrideTypeDefault: + spec.Config = json.RawMessage(row.Options) + + case kolide.OptionOverrideTypePlatform: + spec.Overrides.Platforms[row.OverrideIdentifier] = json.RawMessage(row.Options) + + default: + level.Info(d.logger).Log( + "err", "ignoring unkown override type", + "type", row.OverrideType, + ) + } + } + + return spec, nil +} + +func (d *Datastore) OptionsForPlatform(platform string) (json.RawMessage, error) { + // SQL uses a custom ordering function to return the single correct + // config with the highest precedence override (the FIELD function + // defines this ordering). If there is no override, it returns the + // default. + sql := ` + SELECT * FROM osquery_options + WHERE override_type = ? OR + (override_type = ? AND override_identifier = ?) + ORDER BY FIELD(override_type, ?, ?) + LIMIT 1 + ` + var row optionsRow + err := d.db.Get( + &row, sql, + kolide.OptionOverrideTypeDefault, + kolide.OptionOverrideTypePlatform, platform, + // Order of the following arguments defines precedence of + // overrides. + kolide.OptionOverrideTypePlatform, kolide.OptionOverrideTypeDefault, + ) + if err != nil { + return nil, errors.Wrapf(err, "retrieving osquery options for platform '%s'", platform) + } + + return json.RawMessage(row.Options), nil +} diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 0580119ff..7e9a94026 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -4,10 +4,241 @@ import ( "database/sql" "fmt" + "github.com/jmoiron/sqlx" "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" ) +func (d *Datastore) ApplyPackSpecs(specs []*kolide.PackSpec) (err error) { + tx, err := d.db.Beginx() + if err != nil { + return errors.Wrap(err, "begin ApplyPackSpec transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + for _, spec := range specs { + err = applyPackSpec(tx, spec) + if err != nil { + return errors.Wrapf(err, "applying pack '%s'", spec.Name) + } + } + + err = tx.Commit() + return errors.Wrap(err, "commit transaction") +} + +func applyPackSpec(tx *sqlx.Tx, spec *kolide.PackSpec) error { + if spec.Name == "" { + return errors.New("pack name must not be empty") + } + // Insert/update pack + query := ` + INSERT INTO packs (name, description, platform) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + description = VALUES(description), + platform = VALUES(platform), + deleted = false + ` + if _, err := tx.Exec(query, spec.Name, spec.Description, spec.Platform); err != nil { + return errors.Wrap(err, "insert/update pack") + } + + // Get Pack ID + // This is necessary because MySQL last_insert_id does not return a value + // if no update was made. + var packID uint + query = "SELECT id FROM packs WHERE name = ?" + if err := tx.Get(&packID, query, spec.Name); err != nil { + return errors.Wrap(err, "getting pack ID") + } + + // Delete existing scheduled queries for pack + query = "DELETE FROM scheduled_queries WHERE pack_id = ?" + if _, err := tx.Exec(query, packID); err != nil { + return errors.Wrap(err, "delete existing scheduled queries") + } + + // Insert new scheduled queries for pack + for _, q := range spec.Queries { + query = ` + INSERT INTO scheduled_queries ( + pack_id, query_name, name, description, ` + "`interval`" + `, + snapshot, removed, shard, platform, version + ) + VALUES ( + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ? + ) + ` + _, err := tx.Exec(query, + packID, q.QueryName, q.Name, q.Description, q.Interval, + q.Snapshot, q.Removed, q.Shard, q.Platform, q.Version, + ) + switch { + case isChildForeignKeyError(err): + return errors.Errorf("cannot schedule unknown query '%s'", q.QueryName) + case err != nil: + return errors.Wrapf(err, "adding query %s referencing %s", q.Name, q.QueryName) + } + } + + // Delete existing targets + query = "DELETE FROM pack_targets WHERE pack_id = ?" + if _, err := tx.Exec(query, packID); err != nil { + return errors.Wrap(err, "delete existing targets") + } + + // Insert targets + for _, l := range spec.Targets.Labels { + query = ` + INSERT INTO pack_targets (pack_id, type, target_id) + VALUES (?, ?, (SELECT id FROM labels WHERE name = ?)) + ` + if _, err := tx.Exec(query, packID, kolide.TargetLabel, l); err != nil { + return errors.Wrap(err, "adding label to pack") + } + } + + return nil +} + +func (d *Datastore) GetPackSpecs() (specs []*kolide.PackSpec, err error) { + tx, err := d.db.Beginx() + if err != nil { + return nil, errors.Wrap(err, "begin GetPackSpecs transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + // Get basic specs + query := "SELECT id, name, description, platform FROM packs" + if err := tx.Select(&specs, query); err != nil { + return nil, errors.Wrap(err, "get packs") + } + + // Load targets + for _, spec := range specs { + query = ` +SELECT l.name +FROM labels l JOIN pack_targets pt +WHERE pack_id = ? AND pt.type = ? AND pt.target_id = l.id +` + if err := tx.Select(&spec.Targets.Labels, query, spec.ID, kolide.TargetLabel); err != nil { + return nil, errors.Wrap(err, "get pack targets") + } + } + + // Load queries + for _, spec := range specs { + query = ` +SELECT +query_name, name, description, ` + "`interval`" + `, +snapshot, removed, shard, platform, version +FROM scheduled_queries +WHERE pack_id = ? +` + if err := tx.Select(&spec.Queries, query, spec.ID); err != nil { + return nil, errors.Wrap(err, "get pack queries") + } + } + + err = tx.Commit() + if err != nil { + return nil, errors.Wrap(err, "commit transaction") + } + + return specs, nil +} + +func (d *Datastore) GetPackSpec(name string) (spec *kolide.PackSpec, err error) { + tx, err := d.db.Beginx() + if err != nil { + return nil, errors.Wrap(err, "begin GetPackSpecs transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + // Get basic spec + var specs []*kolide.PackSpec + query := "SELECT id, name, description, platform FROM packs WHERE name = ?" + if err := tx.Select(&specs, query, name); err != nil { + return nil, errors.Wrap(err, "get packs") + } + if len(specs) == 0 { + return nil, notFound("Pack").WithName(name) + } + if len(specs) > 1 { + return nil, errors.Errorf("expected 1 pack row, got %d", len(specs)) + } + + spec = specs[0] + + // Load targets + query = ` +SELECT l.name +FROM labels l JOIN pack_targets pt +WHERE pack_id = ? AND pt.type = ? AND pt.target_id = l.id +` + if err := tx.Select(&spec.Targets.Labels, query, spec.ID, kolide.TargetLabel); err != nil { + return nil, errors.Wrap(err, "get pack targets") + } + + // Load queries + query = ` +SELECT +query_name, name, description, ` + "`interval`" + `, +snapshot, removed, shard, platform, version +FROM scheduled_queries +WHERE pack_id = ? +` + if err := tx.Select(&spec.Queries, query, spec.ID); err != nil { + return nil, errors.Wrap(err, "get pack queries") + } + + err = tx.Commit() + if err != nil { + return nil, errors.Wrap(err, "commit transaction") + } + + return spec, nil +} + func (d *Datastore) PackByName(name string, opts ...kolide.OptionalArg) (*kolide.Pack, bool, error) { db := d.getTransaction(opts) sqlStatement := ` @@ -27,70 +258,9 @@ func (d *Datastore) PackByName(name string, opts ...kolide.OptionalArg) (*kolide return &pack, true, nil } -// NewPack creates a new Pack -func (d *Datastore) NewPack(pack *kolide.Pack, opts ...kolide.OptionalArg) (*kolide.Pack, error) { - db := d.getTransaction(opts) - var ( - deletedPack kolide.Pack - query string - ) - err := db.Get(&deletedPack, - "SELECT * FROM packs WHERE name = ? AND deleted", pack.Name) - switch err { - case nil: - query = ` - REPLACE INTO packs - ( name, description, platform, created_by, disabled, deleted) - VALUES ( ?, ?, ?, ?, ?, ?) - ` - case sql.ErrNoRows: - query = ` - INSERT INTO packs - ( name, description, platform, created_by, disabled, deleted) - VALUES ( ?, ?, ?, ?, ?, ?) - ` - default: - return nil, errors.Wrap(err, "check for existing pack") - } - - deleted := false - result, err := db.Exec(query, pack.Name, pack.Description, pack.Platform, pack.CreatedBy, pack.Disabled, deleted) - if err != nil && isDuplicate(err) { - return nil, alreadyExists("Pack", deletedPack.ID) - } else if err != nil { - return nil, errors.Wrap(err, "creating new pack") - } - - id, _ := result.LastInsertId() - pack.ID = uint(id) - return pack, nil -} - -// SavePack stores changes to pack -func (d *Datastore) SavePack(pack *kolide.Pack) error { - query := ` - UPDATE packs - SET name = ?, platform = ?, disabled = ?, description = ? - WHERE id = ? AND NOT deleted - ` - - results, err := d.db.Exec(query, pack.Name, pack.Platform, pack.Disabled, pack.Description, pack.ID) - if err != nil { - return errors.Wrap(err, "updating pack") - } - rowsAffected, err := results.RowsAffected() - if err != nil { - return errors.Wrap(err, "rows affected updating packs") - } - if rowsAffected == 0 { - return notFound("Pack").WithID(pack.ID) - } - return nil -} - // DeletePack soft deletes a kolide.Pack so that it won't show up in results -func (d *Datastore) DeletePack(pid uint) error { - return d.deleteEntity("packs", pid) +func (d *Datastore) DeletePack(name string) error { + return d.deleteEntityByName("packs", name) } // Pack fetch kolide.Pack with matching ID @@ -118,38 +288,6 @@ func (d *Datastore) ListPacks(opt kolide.ListOptions) ([]*kolide.Pack, error) { return packs, nil } -// AddLabelToPack associates a kolide.Label with a kolide.Pack -func (d *Datastore) AddLabelToPack(lid uint, pid uint, opts ...kolide.OptionalArg) error { - db := d.getTransaction(opts) - - query := ` - INSERT INTO pack_targets ( pack_id, type, target_id ) - VALUES ( ?, ?, ? ) - ON DUPLICATE KEY UPDATE id=id - ` - _, err := db.Exec(query, pid, kolide.TargetLabel, lid) - if err != nil { - return errors.Wrap(err, "adding label to pack") - } - - return nil -} - -// AddHostToPack associates a kolide.Host with a kolide.Pack -func (d *Datastore) AddHostToPack(hid, pid uint) error { - query := ` - INSERT INTO pack_targets ( pack_id, type, target_id ) - VALUES ( ?, ?, ? ) - ON DUPLICATE KEY UPDATE id=id - ` - _, err := d.db.Exec(query, pid, kolide.TargetHost, hid) - if err != nil { - return errors.Wrap(err, "adding host to pack") - } - - return nil -} - // ListLabelsForPack will return a list of kolide.Label records associated with kolide.Pack func (d *Datastore) ListLabelsForPack(pid uint) ([]*kolide.Label, error) { query := ` @@ -180,39 +318,26 @@ func (d *Datastore) ListLabelsForPack(pid uint) ([]*kolide.Label, error) { return labels, nil } -// RemoreLabelFromPack will remove the association between a kolide.Label and -// a kolide.Pack -func (d *Datastore) RemoveLabelFromPack(lid, pid uint) error { +func (d *Datastore) ListPacksForHost(hid uint) ([]*kolide.Pack, error) { query := ` - DELETE FROM pack_targets - WHERE target_id = ? AND pack_id = ? AND type = ? + SELECT DISTINCT p.* + FROM packs p + JOIN pack_targets pt + JOIN label_query_executions lqe + ON ( + p.id = pt.pack_id + AND pt.target_id = lqe.label_id + AND pt.type = ? + AND lqe.matches + ) + WHERE lqe.host_id = ? AND NOT p.disabled ` - _, err := d.db.Exec(query, lid, pid, kolide.TargetLabel) - if err == sql.ErrNoRows { - return notFound("PackTarget").WithMessage(fmt.Sprintf("label ID: %d, pack ID: %d", lid, pid)) - } else if err != nil { - return errors.Wrap(err, "removing label from pack") + + packs := []*kolide.Pack{} + if err := d.db.Select(&packs, query, kolide.TargetLabel, hid); err != nil && err != sql.ErrNoRows { + return nil, errors.Wrap(err, "listing hosts in pack") } - - return nil -} - -// RemoveHostFromPack will remove the association between a kolide.Host and a -// kolide.Pack -func (d *Datastore) RemoveHostFromPack(hid, pid uint) error { - query := ` - DELETE FROM pack_targets - WHERE target_id = ? AND pack_id = ? AND type = ? - ` - _, err := d.db.Exec(query, hid, pid, kolide.TargetHost) - if err == sql.ErrNoRows { - return notFound("PackTarget").WithMessage(fmt.Sprintf("host ID: %d, pack ID: %d", hid, pid)) - } else if err != nil { - return errors.Wrap(err, "removing host from pack") - } - - return nil - + return packs, nil } func (d *Datastore) ListHostsInPack(pid uint, opt kolide.ListOptions) ([]uint, error) { diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index c226a5d11..daaeb527d 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -2,13 +2,69 @@ package mysql import ( "database/sql" + "fmt" "github.com/jmoiron/sqlx" "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" ) -func (d *Datastore) QueryByName(name string, opts ...kolide.OptionalArg) (*kolide.Query, bool, error) { +func (d *Datastore) ApplyQueries(authorID uint, queries []*kolide.Query) (err error) { + tx, err := d.db.Begin() + if err != nil { + return errors.Wrap(err, "begin ApplyQueries transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + sql := ` + INSERT INTO queries ( + name, + description, + query, + author_id, + saved, + deleted + ) VALUES ( ?, ?, ?, ?, true, false ) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + description = VALUES(description), + query = VALUES(query), + author_id = VALUES(author_id), + saved = VALUES(saved), + deleted = VALUES(deleted) + ` + stmt, err := tx.Prepare(sql) + if err != nil { + return errors.Wrap(err, "prepare ApplyQueries insert") + } + + for _, q := range queries { + if q.Name == "" { + return errors.New("query name must not be empty") + } + _, err := stmt.Exec(q.Name, q.Description, q.Query, authorID) + if err != nil { + return errors.Wrap(err, "exec ApplyQueries insert") + } + } + + err = tx.Commit() + return errors.Wrap(err, "commit ApplyQueries transaction") +} + +func (d *Datastore) QueryByName(name string, opts ...kolide.OptionalArg) (*kolide.Query, error) { db := d.getTransaction(opts) sqlStatement := ` SELECT * @@ -19,11 +75,16 @@ func (d *Datastore) QueryByName(name string, opts ...kolide.OptionalArg) (*kolid err := db.Get(&query, sqlStatement, name) if err != nil { if err == sql.ErrNoRows { - return nil, false, nil + return nil, notFound("Query").WithName(name) } - return nil, false, errors.Wrap(err, "selecting query by name") + return nil, errors.Wrap(err, "selecting query by name") } - return &query, true, nil + + if err := d.loadPacksForQueries([]*kolide.Query{&query}); err != nil { + return nil, errors.Wrap(err, "loading packs for query") + } + + return &query, nil } // NewQuery creates a New Query. If a query with the same name was soft-deleted, @@ -99,8 +160,8 @@ func (d *Datastore) SaveQuery(q *kolide.Query) error { } // DeleteQuery soft deletes Query identified by Query.ID -func (d *Datastore) DeleteQuery(qid uint) error { - return d.deleteEntity("queries", qid) +func (d *Datastore) DeleteQuery(name string) error { + return d.deleteEntityByName("queries", name) } // DeleteQueries (soft) deletes the existing query objects with the provided @@ -175,7 +236,6 @@ func (d *Datastore) ListQueries(opt kolide.ListOptions) ([]*kolide.Query, error) } return results, nil - } // loadPacksForQueries loads the packs associated with the provided queries @@ -185,31 +245,31 @@ func (d *Datastore) loadPacksForQueries(queries []*kolide.Query) error { } sql := ` - SELECT p.*, sq.query_id AS query_id + SELECT p.*, sq.query_name AS query_name FROM packs p JOIN scheduled_queries sq ON p.id = sq.pack_id - WHERE query_id IN (?) + WHERE query_name IN (?) AND NOT p.deleted ` // Used to map the results - id_queries := map[uint]*kolide.Query{} + name_queries := map[string]*kolide.Query{} // Used for the IN clause - ids := []uint{} + names := []string{} for _, q := range queries { q.Packs = make([]kolide.Pack, 0) - ids = append(ids, q.ID) - id_queries[q.ID] = q + names = append(names, q.Name) + name_queries[q.Name] = q } - query, args, err := sqlx.In(sql, ids) + query, args, err := sqlx.In(sql, names) if err != nil { return errors.Wrap(err, "building query in load packs for queries") } rows := []struct { - QueryID uint `db:"query_id"` + QueryName string `db:"query_name"` kolide.Pack }{} @@ -219,7 +279,7 @@ func (d *Datastore) loadPacksForQueries(queries []*kolide.Query) error { } for _, row := range rows { - q := id_queries[row.QueryID] + q := name_queries[row.QueryName] q.Packs = append(q.Packs, row.Pack) } diff --git a/server/datastore/mysql/scheduled_queries.go b/server/datastore/mysql/scheduled_queries.go index 83d00e2d0..15be0a2c5 100644 --- a/server/datastore/mysql/scheduled_queries.go +++ b/server/datastore/mysql/scheduled_queries.go @@ -1,104 +1,29 @@ package mysql import ( - "database/sql" - "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" ) -func (d *Datastore) NewScheduledQuery(sq *kolide.ScheduledQuery, opts ...kolide.OptionalArg) (*kolide.ScheduledQuery, error) { - db := d.getTransaction(opts) - query := ` - INSERT INTO scheduled_queries ( - pack_id, - query_id, - snapshot, - removed, - ` + "`interval`" + `, - platform, - version, - shard - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ` - result, err := db.Exec(query, sq.PackID, sq.QueryID, sq.Snapshot, sq.Removed, sq.Interval, sq.Platform, sq.Version, sq.Shard) - if err != nil { - return nil, errors.Wrap(err, "inserting scheduled query") - } - - id, _ := result.LastInsertId() - sq.ID = uint(id) - - query = `SELECT query, name FROM queries WHERE id = ? LIMIT 1` - metadata := []struct { - Query string - Name string - }{} - - err = db.Select(&metadata, query, sq.QueryID) - if err != nil && err == sql.ErrNoRows { - return nil, notFound("Query").WithID(sq.QueryID) - } else if err != nil { - return nil, errors.Wrap(err, "select query by ID") - } - - if len(metadata) != 1 { - return nil, errors.Wrap(err, "wrong number of results returned from database") - } - - sq.Query = metadata[0].Query - sq.Name = metadata[0].Name - - return sq, nil -} - -func (d *Datastore) SaveScheduledQuery(sq *kolide.ScheduledQuery) (*kolide.ScheduledQuery, error) { - query := ` - UPDATE scheduled_queries - SET pack_id = ?, query_id = ?, ` + "`interval`" + ` = ?, snapshot = ?, removed = ?, platform = ?, version = ?, shard = ? - WHERE id = ? AND NOT deleted - ` - result, err := d.db.Exec(query, sq.PackID, sq.QueryID, sq.Interval, sq.Snapshot, sq.Removed, sq.Platform, sq.Version, sq.Shard, sq.ID) - if err != nil { - return nil, errors.Wrap(err, "saving a scheduled query") - } - rows, err := result.RowsAffected() - if err != nil { - return nil, errors.Wrap(err, "rows affected saving a scheduled query") - } - if rows == 0 { - return nil, notFound("ScheduledQueries").WithID(sq.ID) - } - return sq, nil -} - -func (d *Datastore) DeleteScheduledQuery(id uint) error { - return d.deleteEntity("scheduled_queries", id) -} - -func (d *Datastore) ScheduledQuery(id uint) (*kolide.ScheduledQuery, error) { - query := ` - SELECT sq.*, q.query, q.name - FROM scheduled_queries sq - JOIN queries q - ON sq.query_id = q.id - WHERE sq.id = ? - AND NOT sq.deleted - ` - sq := &kolide.ScheduledQuery{} - if err := d.db.Get(sq, query, id); err != nil { - return nil, errors.Wrap(err, "selecting a scheduled query") - } - - return sq, nil -} - func (d *Datastore) ListScheduledQueriesInPack(id uint, opts kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { query := ` - SELECT sq.*, q.query, q.name + SELECT + sq.id, + sq.pack_id, + COALESCE(sq.name, q.name) AS name, + sq.query_name, + COALESCE(sq.description, '') AS description, + sq.interval, + sq.snapshot, + sq.removed, + COALESCE(sq.platform, '') AS platform, + sq.version, + sq.shard, + q.query, + q.id AS query_id FROM scheduled_queries sq JOIN queries q - ON sq.query_id = q.id + ON sq.query_name = q.name WHERE sq.pack_id = ? AND NOT sq.deleted ` diff --git a/server/kolide/app.go b/server/kolide/app.go index 82776096a..b6d554134 100644 --- a/server/kolide/app.go +++ b/server/kolide/app.go @@ -209,14 +209,14 @@ type AppConfigPayload struct { // OrgInfo contains general info about the organization using Kolide. type OrgInfo struct { - OrgName *string `json:"org_name"` - OrgLogoURL *string `json:"org_logo_url"` + OrgName *string `json:"org_name,omitempty"` + OrgLogoURL *string `json:"org_logo_url,omitempty"` } // ServerSettings contains general settings about the kolide App. type ServerSettings struct { - KolideServerURL *string `json:"kolide_server_url"` - EnrollSecret *string `json:"osquery_enroll_secret"` + KolideServerURL *string `json:"kolide_server_url,omitempty"` + EnrollSecret *string `json:"osquery_enroll_secret,omitempty"` } type OrderDirection int diff --git a/server/kolide/campaigns.go b/server/kolide/campaigns.go index b916cb8ff..c1731a13c 100644 --- a/server/kolide/campaigns.go +++ b/server/kolide/campaigns.go @@ -44,6 +44,10 @@ type CampaignStore interface { // CampaignService defines the distributed query campaign related service // methods type CampaignService interface { + // NewDistributedQueryCampaign creates a new distributed query campaign + // with the provided query and host/label targets (specified by name). + NewDistributedQueryCampaignByNames(ctx context.Context, queryString string, hosts []string, labels []string) (*DistributedQueryCampaign, error) + // NewDistributedQueryCampaign creates a new distributed query campaign // with the provided query and host/label targets NewDistributedQueryCampaign(ctx context.Context, queryString string, hosts []uint, labels []uint) (*DistributedQueryCampaign, error) diff --git a/server/kolide/datastore.go b/server/kolide/datastore.go index 7df773d38..b83b089fc 100644 --- a/server/kolide/datastore.go +++ b/server/kolide/datastore.go @@ -15,9 +15,9 @@ type Datastore interface { InviteStore ScheduledQueryStore OptionStore - DecoratorStore FileIntegrityMonitoringStore YARAStore + OsqueryOptionsStore Name() string Drop() error // MigrateTables creates and migrates the table schemas @@ -44,6 +44,14 @@ type NotFoundError interface { IsNotFound() bool } +func IsNotFound(err error) bool { + e, ok := err.(NotFoundError) + if !ok { + return false + } + return e.IsNotFound() +} + // AlreadyExists is returned when creating a datastore resource that already // exists. type AlreadyExistsError interface { diff --git a/server/kolide/decorators.go b/server/kolide/decorators.go index c958cd780..a664edfbc 100644 --- a/server/kolide/decorators.go +++ b/server/kolide/decorators.go @@ -1,38 +1,13 @@ package kolide import ( - "context" "errors" "strings" ) -// DecoratorStore methods to manipulate decorator queries. -// See https://osquery.readthedocs.io/en/stable/deployment/configuration/ -type DecoratorStore interface { - // NewDecorator creates a decorator query. - NewDecorator(decorator *Decorator, opts ...OptionalArg) (*Decorator, error) - // DeleteDecorator removes a decorator query. - DeleteDecorator(id uint) error - // Decorator retrieves a decorator query with supplied ID. - Decorator(id uint) (*Decorator, error) - // ListDecorators returns all decorator queries. - ListDecorators(opts ...OptionalArg) ([]*Decorator, error) - // SaveDecorator updates an existing decorator - SaveDecorator(dec *Decorator, opts ...OptionalArg) error -} - -// DecoratorService exposes decorators data so it can be manipulated by -// end users -type DecoratorService interface { - // ListDecorators returns decorators - ListDecorators(ctx context.Context) ([]*Decorator, error) - // DeleteDecorator removes an existing decorator if it is not built-in - DeleteDecorator(ctx context.Context, id uint) error - // NewDecorator creates a new decorator - NewDecorator(ctx context.Context, payload DecoratorPayload) (*Decorator, error) - // ModifyDecorator updates an existing decorator - ModifyDecorator(ctx context.Context, payload DecoratorPayload) (*Decorator, error) -} +// DEPRECATED +// Decorators are now stored as JSON in the config, so these types are only +// useful for migrating existing Fleet installations. // DecoratorType refers to the allowable types of decorator queries. // See https://osquery.readthedocs.io/en/stable/deployment/configuration/ diff --git a/server/kolide/fleetctl.go b/server/kolide/fleetctl.go new file mode 100644 index 000000000..fb8be28e7 --- /dev/null +++ b/server/kolide/fleetctl.go @@ -0,0 +1,3 @@ +package kolide + +const ApiVersion = "v1" diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index e61c06665..cfc5310ef 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -4,9 +4,9 @@ import ( "context" "crypto/rand" "encoding/base64" + "net" + "strings" "time" - "net" - "strings" ) const ( @@ -50,6 +50,8 @@ type HostStore interface { // given host should run. The result map is a mapping from campaign ID // to query text. DistributedQueriesForHost(host *Host) (map[uint]string, error) + // HostIDsByName Retrieve the IDs associated with the given hostnames + HostIDsByName(hostnames []string) ([]uint, error) } type HostService interface { @@ -134,18 +136,18 @@ func (h *Host) ResetPrimaryNetwork() bool { // nics are in descending order of IO // so we default to the most active nic if len(h.NetworkInterfaces) > 0 { - // Check IPv4 address + // Check IPv4 address for _, nic := range h.NetworkInterfaces { - if strings.Index(nic.IPAddress, "127.") == 0 { - continue - } - var isIpAddress = net.ParseIP(nic.IPAddress) - if isIpAddress.To4() != nil { - h.PrimaryNetworkInterfaceID = &nic.ID - return true - } + if strings.Index(nic.IPAddress, "127.") == 0 { + continue + } + var isIpAddress = net.ParseIP(nic.IPAddress) + if isIpAddress.To4() != nil { + h.PrimaryNetworkInterfaceID = &nic.ID + return true + } } - // return IPv6 or other nic in place of IPv4 + // return IPv6 or other nic in place of IPv4 h.PrimaryNetworkInterfaceID = &h.NetworkInterfaces[0].ID return true } diff --git a/server/kolide/labels.go b/server/kolide/labels.go index f6418138f..43afc8e4f 100644 --- a/server/kolide/labels.go +++ b/server/kolide/labels.go @@ -6,9 +6,16 @@ import ( ) type LabelStore interface { + // ApplyLabelSpecs applies a list of LabelSpecs to the datastore, + // creating and updating labels as necessary. + ApplyLabelSpecs(specs []*LabelSpec) error + // GetLabelSpecs returns all of the stored LabelSpecs. + GetLabelSpecs() ([]*LabelSpec, error) + // GetLabelSpec returns the spec for the named label. + GetLabelSpec(name string) (*LabelSpec, error) + // Label methods - NewLabel(Label *Label, opts ...OptionalArg) (*Label, error) - DeleteLabel(lid uint) error + DeleteLabel(name string) error Label(lid uint) (*Label, error) ListLabels(opt ListOptions) ([]*Label, error) @@ -39,30 +46,29 @@ type LabelStore interface { SearchLabels(query string, omit ...uint) ([]Label, error) - // SaveLabel allows modification of a label's name and/or description - SaveLabel(label *Label) (*Label, error) + // LabelIDsByName Retrieve the IDs associated with the given labels + LabelIDsByName(labels []string) ([]uint, error) } type LabelService interface { + // ApplyLabelSpecs applies a list of LabelSpecs to the datastore, + // creating and updating labels as necessary. + ApplyLabelSpecs(ctx context.Context, specs []*LabelSpec) error + // GetLabelSpecs returns all of the stored LabelSpecs. + GetLabelSpecs(ctx context.Context) ([]*LabelSpec, error) + // GetLabelSpec gets the spec for the label with the given name. + GetLabelSpec(ctx context.Context, name string) (*LabelSpec, error) + ListLabels(ctx context.Context, opt ListOptions) (labels []*Label, err error) GetLabel(ctx context.Context, id uint) (label *Label, err error) - NewLabel(ctx context.Context, p LabelPayload) (label *Label, err error) - DeleteLabel(ctx context.Context, id uint) (err error) - // ModifyLabel is used to change editable fields belonging to a Label - ModifyLabel(ctx context.Context, id uint, payload ModifyLabelPayload) (*Label, error) + DeleteLabel(ctx context.Context, name string) (err error) // HostIDsForLabel returns ids of hosts that belong to the label identified // by lid HostIDsForLabel(lid uint) ([]uint, error) } -// ModifyLabelPayload is used to change editable fields for a Label -type ModifyLabelPayload struct { - Name *string `json:"name"` - Description *string `json:"description"` -} - type LabelPayload struct { Name *string `json:"name"` Query *string `json:"query"` @@ -99,3 +105,12 @@ type LabelQueryExecution struct { LabelID uint HostID uint } + +type LabelSpec struct { + ID uint + Name string `json:"name"` + Description string `json:"description"` + Query string `json:"query"` + Platform string `json:"platform,omitempty"` + LabelType LabelType `json:"label_type" db:"label_type"` +} diff --git a/server/kolide/meta.go b/server/kolide/meta.go new file mode 100644 index 000000000..de8700067 --- /dev/null +++ b/server/kolide/meta.go @@ -0,0 +1,6 @@ +package kolide + +type ObjectMetadata struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} diff --git a/server/kolide/osquery.go b/server/kolide/osquery.go index 4b1111975..eb67781ca 100644 --- a/server/kolide/osquery.go +++ b/server/kolide/osquery.go @@ -8,7 +8,7 @@ import ( type OsqueryService interface { EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string) (nodeKey string, err error) AuthenticateHost(ctx context.Context, nodeKey string) (host *Host, err error) - GetClientConfig(ctx context.Context) (config *OsqueryConfig, err error) + GetClientConfig(ctx context.Context) (config map[string]interface{}, err error) // GetDistributedQueries retrieves the distributed queries to run for // the host in the provided context. These may be detail queries, label // queries, or user-initiated distributed queries. A map from query @@ -47,9 +47,16 @@ type QueryContent struct { Shard *uint `json:"shard,omitempty"` } +type PermissiveQueryContent struct { + QueryContent + Interval interface{} `json:"interval"` +} + // Queries is a helper which represents the format of a set of queries in a pack. type Queries map[string]QueryContent +type PermissiveQueries map[string]PermissiveQueryContent + // PackContent is the format of an osquery query pack. type PackContent struct { Platform string `json:"platform,omitempty"` @@ -59,9 +66,19 @@ type PackContent struct { Queries Queries `json:"queries"` } +type PermissivePackContent struct { + Platform string `json:"platform,omitempty"` + Version string `json:"version,omitempty"` + Shard uint `json:"shard,omitempty"` + Discovery []string `json:"discovery,omitempty"` + Queries PermissiveQueries `json:"queries"` +} + // Packs is a helper which represents the format of a list of osquery query packs. type Packs map[string]PackContent +type PermissivePacks map[string]PermissivePackContent + // Decorators is the format of the decorator configuration in an osquery config. type Decorators struct { Load []string `json:"load,omitempty"` @@ -80,3 +97,8 @@ type OsqueryConfig struct { // FIM (File Integrity Monitoring) FilePaths FIMSections `json:"file_paths,omitempty"` } + +type PermissiveOsqueryConfig struct { + OsqueryConfig + Packs PermissivePacks `jsoon:"packs,omitempty"` +} diff --git a/server/kolide/osquery_options.go b/server/kolide/osquery_options.go new file mode 100644 index 000000000..c6835b5a8 --- /dev/null +++ b/server/kolide/osquery_options.go @@ -0,0 +1,50 @@ +package kolide + +import ( + "context" + "encoding/json" +) + +type OsqueryOptionsStore interface { + ApplyOptions(options *OptionsSpec) error + GetOptions() (*OptionsSpec, error) + OptionsForPlatform(platform string) (json.RawMessage, error) +} + +type OsqueryOptionsService interface { + ApplyOptionsSpec(ctx context.Context, spec *OptionsSpec) error + GetOptionsSpec(ctx context.Context) (*OptionsSpec, error) +} + +type OptionsObject struct { + ObjectMetadata + Spec OptionsSpec `json:"spec"` +} + +type OptionsSpec struct { + Config json.RawMessage `json:"config"` + Overrides OptionsOverrides `json:"overrides,omitempty"` +} + +type OptionsOverrides struct { + Platforms map[string]json.RawMessage `json:"platforms,omitempty"` +} + +const ( + OptionsKind = "Options" +) + +// OptionOverrideType is used to designate which override type a given set of +// options is used for. Currently the only supported override type is by +// platform. +type OptionOverrideType int + +const ( + // OptionOverrideTypeDefault indicates that this is the default config + // (provided to hosts when there is no override set for them). + OptionOverrideTypeDefault OptionOverrideType = iota + // OptionOverrideTypePlatform indicates that this is a + // platform-specific config override (with precedence over the default + // config). + OptionOverrideTypePlatform +) diff --git a/server/kolide/osquery_options_test.go b/server/kolide/osquery_options_test.go new file mode 100644 index 000000000..b0a96cf45 --- /dev/null +++ b/server/kolide/osquery_options_test.go @@ -0,0 +1,119 @@ +package kolide + +import ( + "testing" + + "github.com/ghodss/yaml" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalYaml(t *testing.T) { + y := []byte(` +apiVersion: k8s.kolide.com/v1alpha1 +kind: OsqueryOptions +spec: + config: + options: + distributed_interval: 3 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 10 + overrides: + platforms: + darwin: + options: + distributed_interval: 10 + distributed_tls_max_attempts: 10 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 300 + disable_tables: chrome_extensions + docker_socket: /var/run/docker.sock + file_paths: + users: + - /Users/%/Library/%% + - /Users/%/Documents/%% + etc: + - /etc/%% + linux: + options: + distributed_interval: 10 + distributed_tls_max_attempts: 3 + logger_plugin: tls + logger_tls_endpoint: /api/v1/osquery/log + logger_tls_period: 60 + schedule_timeout: 60 + docker_socket: /etc/run/docker.sock + frobulations: + - fire + - ice +`) + + expectedConfig := `{ + "options":{ + "distributed_interval":3, + "distributed_tls_max_attempts":3, + "logger_plugin":"tls", + "logger_tls_endpoint":"/api/v1/osquery/log", + "logger_tls_period":10 + } +}` + + expectedDarwin := `{ + "options":{ + "disable_tables":"chrome_extensions", + "distributed_interval":10, + "distributed_tls_max_attempts":10, + "docker_socket":"/var/run/docker.sock", + "logger_plugin":"tls", + "logger_tls_endpoint":"/api/v1/osquery/log", + "logger_tls_period":300 + }, + "file_paths":{ + "etc":[ + "/etc/%%" + ], + "users":[ + "/Users/%/Library/%%", + "/Users/%/Documents/%%" + ] + } +}` + + expectedLinux := `{ + "options":{ + "distributed_interval":10, + "distributed_tls_max_attempts":3, + "docker_socket":"/etc/run/docker.sock", + "logger_plugin":"tls", + "logger_tls_endpoint":"/api/v1/osquery/log", + "logger_tls_period":60, + "schedule_timeout":60 + }, + "frobulations": [ + "fire", + "ice" + ] +}` + + var foo OptionsObject + err := yaml.Unmarshal(y, &foo) + + require.Nil(t, err) + + assert.JSONEq(t, expectedConfig, string(foo.Spec.Config)) + + platformOverrides := foo.Spec.Overrides.Platforms + assert.Len(t, platformOverrides, 2) + + if assert.Contains(t, platformOverrides, "darwin") { + assert.JSONEq(t, expectedDarwin, string(platformOverrides["darwin"])) + } + + if assert.Contains(t, platformOverrides, "linux") { + assert.JSONEq(t, expectedLinux, string(platformOverrides["linux"])) + } +} diff --git a/server/kolide/packs.go b/server/kolide/packs.go index c82b9dd60..fe7b22645 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -6,43 +6,35 @@ import ( // PackStore is the datastore interface for managing query packs. type PackStore interface { - // NewPack creates a new pack in the datastore. - NewPack(pack *Pack, opts ...OptionalArg) (*Pack, error) - - // SavePack updates an existing pack in the datastore. - SavePack(pack *Pack) error + // ApplyPackSpecs applies a list of PackSpecs to the datastore, + // creating and updating packs as necessary. + ApplyPackSpecs(specs []*PackSpec) error + // GetPackSpecs returns all of the stored PackSpecs. + GetPackSpecs() ([]*PackSpec, error) + // GetPackSpec returns the spec for the named pack. + GetPackSpec(name string) (*PackSpec, error) // DeletePack deletes a pack record from the datastore. - DeletePack(pid uint) error + DeletePack(name string) error // Pack retrieves a pack from the datastore by ID. Pack(pid uint) (*Pack, error) // ListPacks lists all packs in the datastore. ListPacks(opt ListOptions) ([]*Pack, error) + // PackByName fetches pack if it exists, if the pack // exists the bool return value is true PackByName(name string, opts ...OptionalArg) (*Pack, bool, error) - // AddLabelToPack adds an existing label to an existing pack, both by ID. - AddLabelToPack(lid, pid uint, opts ...OptionalArg) error - - // RemoveLabelFromPack removes an existing label from it's association with - // an existing pack, both by ID. - RemoveLabelFromPack(lid, pid uint) error - // ListLabelsForPack lists all labels that are associated with a pack. ListLabelsForPack(pid uint) ([]*Label, error) - // AddHostToPack adds an existing host to an existing pack, both by ID. - AddHostToPack(hid uint, pid uint) error + // ListPacksForHost lists the packs that a host should execute. + ListPacksForHost(hid uint) (packs []*Pack, err error) - // RemoveHostFromPack removes an existing host from it's association with - // an existing pack, both by ID. - RemoveHostFromPack(hid uint, pid uint) error - - // ListHostsInPack lists the IDs of all hosts that are associated with a pack, - // both through labels and manual associations. + // ListHostsInPack lists the IDs of all hosts that are associated with a pack + // through labels. ListHostsInPack(pid uint, opt ListOptions) ([]uint, error) // ListExplicitHostsInPack lists the IDs of hosts that have been manually @@ -52,38 +44,26 @@ type PackStore interface { // PackService is the service interface for managing query packs. type PackService interface { + // ApplyPackSpecs applies a list of PackSpecs to the datastore, + // creating and updating packs as necessary. + ApplyPackSpecs(ctx context.Context, specs []*PackSpec) error + // GetPackSpecs returns all of the stored PackSpecs. + GetPackSpecs(ctx context.Context) ([]*PackSpec, error) + // GetPackSpec gets the spec for the pack with the given name. + GetPackSpec(ctx context.Context, name string) (*PackSpec, error) + // ListPacks lists all packs in the application. ListPacks(ctx context.Context, opt ListOptions) (packs []*Pack, err error) // GetPack retrieves a pack by ID. GetPack(ctx context.Context, id uint) (pack *Pack, err error) - // NewPack creates a new pack in the datastore. - NewPack(ctx context.Context, p PackPayload) (pack *Pack, err error) - - // ModifyPack modifies an existing pack in the datastore. - ModifyPack(ctx context.Context, id uint, p PackPayload) (pack *Pack, err error) - // DeletePack deletes a pack record from the datastore. - DeletePack(ctx context.Context, id uint) (err error) - - // AddLabelToPack adds an existing label to an existing pack, both by ID. - AddLabelToPack(ctx context.Context, lid, pid uint) (err error) - - // RemoveLabelFromPack removes an existing label from it's association with - // an existing pack, both by ID. - RemoveLabelFromPack(ctx context.Context, lid, pid uint) (err error) + DeletePack(ctx context.Context, name string) (err error) // ListLabelsForPack lists all labels that are associated with a pack. ListLabelsForPack(ctx context.Context, pid uint) (labels []*Label, err error) - // AddHostToPack adds an existing host to an existing pack, both by ID. - AddHostToPack(ctx context.Context, hid, pid uint) (err error) - - // RemoveHostFromPack removes an existing host from it's association with - // an existing pack, both by ID. - RemoveHostFromPack(ctx context.Context, hid, pid uint) (err error) - // ListPacksForHost lists the packs that a host should execute. ListPacksForHost(ctx context.Context, hid uint) (packs []*Pack, err error) @@ -104,7 +84,6 @@ type Pack struct { Name string `json:"name"` Description string `json:"description"` Platform string `json:"platform"` - CreatedBy uint `json:"created_by" db:"created_by"` Disabled bool `json:"disabled"` } @@ -118,6 +97,31 @@ type PackPayload struct { LabelIDs *[]uint `json:"label_ids"` } +type PackSpec struct { + ID uint `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Platform string `json:"platform,omitempty"` + Targets PackSpecTargets `json:"targets,omitempty"` + Queries []PackSpecQuery `json:"queries,omitempty"` +} + +type PackSpecTargets struct { + Labels []string `json:"labels"` +} + +type PackSpecQuery struct { + QueryName string `json:"query" db:"query_name"` + Name string `json:"name"` + Description string `json:"description"` + Interval uint `json:"interval"` + Snapshot *bool `json:"snapshot,omitempty"` + Removed *bool `json:"removed,omitempty"` + Shard *uint `json:"shard,omitempty"` + Platform *string `json:"platform,omitempty"` + Version *string `json:"version,omitempty"` +} + // PackTarget associates a pack with either a host or a label type PackTarget struct { ID uint diff --git a/server/kolide/queries.go b/server/kolide/queries.go index 6de271f68..c018b6297 100644 --- a/server/kolide/queries.go +++ b/server/kolide/queries.go @@ -2,16 +2,26 @@ package kolide import ( "context" + "strings" + + "github.com/pkg/errors" + + "github.com/ghodss/yaml" ) type QueryStore interface { + // ApplyQueries applies a list of queries (likely from a yaml file) to + // the datastore. Existing queries are updated, and new queries are + // created. + ApplyQueries(authorID uint, queries []*Query) error + // NewQuery creates a new query object in thie datastore. The returned // query should have the ID updated. NewQuery(query *Query, opts ...OptionalArg) (*Query, error) // SaveQuery saves changes to an existing query object. SaveQuery(query *Query) error - // DeleteQuery (soft) deletes an existing query object. - DeleteQuery(qid uint) error + // DeleteQuery deletes an existing query object. + DeleteQuery(name string) error // DeleteQueries (soft) deletes the existing query objects with the // provided IDs. The number of deleted queries is returned along with // any error. @@ -22,12 +32,19 @@ type QueryStore interface { // ListQueries returns a list of queries with the provided sorting and // paging options. Associated packs should also be loaded. ListQueries(opt ListOptions) ([]*Query, error) - // QueryByName looks up a query by name, the second bool is true if a query - // by the name exists. - QueryByName(name string, opts ...OptionalArg) (*Query, bool, error) + // QueryByName looks up a query by name. + QueryByName(name string, opts ...OptionalArg) (*Query, error) } type QueryService interface { + // ApplyQuerySpecs applies a list of queries (creating or updating + // them as necessary) + ApplyQuerySpecs(ctx context.Context, specs []*QuerySpec) error + // GetQuerySpecs gets the YAML file representing all the stored queries. + GetQuerySpecs(ctx context.Context) ([]*QuerySpec, error) + // GetQuerySpec gets the spec for the query with the given name. + GetQuerySpec(ctx context.Context, name string) (*QuerySpec, error) + // ListQueries returns a list of saved queries. Note only saved queries // should be returned (those that are created for distributed queries // but not saved should not be returned). @@ -35,7 +52,7 @@ type QueryService interface { GetQuery(ctx context.Context, id uint) (*Query, error) NewQuery(ctx context.Context, p QueryPayload) (*Query, error) ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error) - DeleteQuery(ctx context.Context, id uint) error + DeleteQuery(ctx context.Context, name string) error // DeleteQueries (soft) deletes the existing query objects with the // provided IDs. The number of deleted queries is returned along with // any error. @@ -64,3 +81,63 @@ type Query struct { // table in the MySQL backend. Packs []Pack `json:"packs" db:"-"` } + +const ( + QueryKind = "Query" +) + +type QueryObject struct { + ObjectMetadata + Spec QuerySpec `json:"spec"` +} + +type QuerySpec struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Query string `json:"query"` +} + +func LoadQueriesFromYaml(yml string) ([]*Query, error) { + queries := []*Query{} + for _, s := range strings.Split(yml, "---") { + s = strings.TrimSpace(s) + if len(s) == 0 { + continue + } + + var q QueryObject + err := yaml.Unmarshal([]byte(s), &q) + if err != nil { + return nil, errors.Wrap(err, "unmarshal yaml") + } + queries = append(queries, + &Query{Name: q.Spec.Name, Description: q.Spec.Description, Query: q.Spec.Query}, + ) + } + + return queries, nil +} + +func WriteQueriesToYaml(queries []*Query) (string, error) { + ymlStrings := []string{} + for _, q := range queries { + qYaml := QueryObject{ + ObjectMetadata: ObjectMetadata{ + ApiVersion: ApiVersion, + Kind: QueryKind, + }, + Spec: QuerySpec{ + Name: q.Name, + Description: q.Description, + Query: q.Query, + }, + } + yml, err := yaml.Marshal(qYaml) + if err != nil { + return "", errors.Wrap(err, "marshal YAML") + } + ymlStrings = append(ymlStrings, string(yml)) + } + + return strings.Join(ymlStrings, "---\n"), nil +} diff --git a/server/kolide/queries_test.go b/server/kolide/queries_test.go new file mode 100644 index 000000000..4966ec287 --- /dev/null +++ b/server/kolide/queries_test.go @@ -0,0 +1,104 @@ +package kolide + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadQueriesFromYamlStrings(t *testing.T) { + var testCases = []struct { + yaml string + queries []*Query + shouldErr bool + }{ + {"notyaml", []*Query{}, true}, + {"", []*Query{}, false}, + {"---", []*Query{}, false}, + { + ` +--- +apiVersion: k8s.kolide.com/v1alpha1 +kind: OsqueryQuery +spec: + name: osquery_version + description: The version of the Launcher and Osquery process + query: select launcher.version, osquery.version from kolide_launcher_info + support: + launcher: 0.3.0 + osquery: 2.9.0 +--- +apiVersion: k8s.kolide.com/v1alpha1 +kind: OsqueryQuery +spec: + name: osquery_schedule + description: Report performance stats for each file in the query schedule. + query: select name, interval, executions, output_size, wall_time, (user_time +--- + +apiVersion: k8s.kolide.com/v1alpha1 +kind: OsqueryQuery +spec: + name: foobar + description: froobing + query: select fizz from frog + +`, + []*Query{ + &Query{ + Name: "osquery_version", + Description: "The version of the Launcher and Osquery process", + Query: "select launcher.version, osquery.version from kolide_launcher_info", + }, + &Query{ + Name: "osquery_schedule", + Description: "Report performance stats for each file in the query schedule.", + Query: "select name, interval, executions, output_size, wall_time, (user_time", + }, + &Query{ + Name: "foobar", + Description: "froobing", + Query: "select fizz from frog", + }, + }, + false, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + queries, err := LoadQueriesFromYaml(tt.yaml) + if tt.shouldErr { + require.NotNil(t, err) + } else { + require.Nil(t, err) + assert.Equal(t, tt.queries, queries) + } + }) + } +} + +func TestRoundtripQueriesYaml(t *testing.T) { + var testCases = []struct{ queries []*Query }{ + {[]*Query{{Name: "froob", Description: "bing", Query: "blong"}}}, + { + []*Query{ + {Name: "froob", Description: "bing", Query: "blong"}, + {Name: "mant", Description: "smump", Query: "tmit"}, + {Name: "gorm", Description: "", Query: "blirz"}, + {Name: "blob", Description: "shmoo", Query: "smarle"}, + }, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + yml, err := WriteQueriesToYaml(tt.queries) + require.Nil(t, err) + queries, err := LoadQueriesFromYaml(yml) + require.Nil(t, err) + assert.Equal(t, tt.queries, queries) + }) + } +} diff --git a/server/kolide/scheduled_queries.go b/server/kolide/scheduled_queries.go index ffd5580a5..2e838bdec 100644 --- a/server/kolide/scheduled_queries.go +++ b/server/kolide/scheduled_queries.go @@ -5,35 +5,29 @@ import ( ) type ScheduledQueryStore interface { - NewScheduledQuery(sq *ScheduledQuery, opts ...OptionalArg) (*ScheduledQuery, error) - SaveScheduledQuery(sq *ScheduledQuery) (*ScheduledQuery, error) - DeleteScheduledQuery(id uint) error - ScheduledQuery(id uint) (*ScheduledQuery, error) ListScheduledQueriesInPack(id uint, opts ListOptions) ([]*ScheduledQuery, error) } type ScheduledQueryService interface { - GetScheduledQuery(ctx context.Context, id uint) (query *ScheduledQuery, err error) GetScheduledQueriesInPack(ctx context.Context, id uint, opts ListOptions) (queries []*ScheduledQuery, err error) - ScheduleQuery(ctx context.Context, sq *ScheduledQuery) (query *ScheduledQuery, err error) - DeleteScheduledQuery(ctx context.Context, id uint) (err error) - ModifyScheduledQuery(ctx context.Context, id uint, p ScheduledQueryPayload) (query *ScheduledQuery, err error) } type ScheduledQuery struct { UpdateCreateTimestamps DeleteFields - ID uint `json:"id"` - PackID uint `json:"pack_id" db:"pack_id"` - QueryID uint `json:"query_id" db:"query_id"` - Query string `json:"query"` // populated via a join on queries - Name string `json:"name"` // populated via a join on queries - Interval uint `json:"interval"` - Snapshot *bool `json:"snapshot"` - Removed *bool `json:"removed"` - Platform *string `json:"platform"` - Version *string `json:"version"` - Shard *uint `json:"shard"` + ID uint `json:"id"` + PackID uint `json:"pack_id" db:"pack_id"` + Name string `json:"name"` + QueryID uint `json:"query_id" db:"query_id"` + QueryName string `json:"query_name" db:"query_name"` + Query string `json:"query"` // populated via a join on queries + Description string `json:"description"` + Interval uint `json:"interval"` + Snapshot *bool `json:"snapshot"` + Removed *bool `json:"removed"` + Platform *string `json:"platform"` + Version *string `json:"version"` + Shard *uint `json:"shard"` } type ScheduledQueryPayload struct { diff --git a/server/kolide/service.go b/server/kolide/service.go index a94601089..9fd45f85a 100644 --- a/server/kolide/service.go +++ b/server/kolide/service.go @@ -9,13 +9,12 @@ type Service interface { QueryService CampaignService OsqueryService + OsqueryOptionsService HostService AppConfigService InviteService TargetService ScheduledQueryService OptionService - ImportConfigService - DecoratorService FileIntegrityMonitoringService } diff --git a/server/kolide/users.go b/server/kolide/users.go index 222000262..2b50ba26c 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -107,17 +107,17 @@ type User struct { // UserPayload is used to modify an existing user type UserPayload struct { - Username *string `json:"username"` - Name *string `json:"name"` - Email *string `json:"email"` - Admin *bool `json:"admin"` - Enabled *bool `json:"enabled"` - Password *string `json:"password"` - GravatarURL *string `json:"gravatar_url"` - Position *string `json:"position"` - InviteToken *string `json:"invite_token"` - SSOInvite *bool `json:"sso_invite"` - SSOEnabled *bool `json:"sso_enabled"` + 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"` } // User creates a user from payload. diff --git a/server/launcher/launcher.go b/server/launcher/launcher.go index be8c3e944..2a0180a28 100644 --- a/server/launcher/launcher.go +++ b/server/launcher/launcher.go @@ -44,10 +44,10 @@ func (svc *launcherWrapper) RequestConfig(ctx context.Context, nodeKey string) ( return "", false, errors.Wrap(err, "get config for launcher") } - // Launcher manages plugins so remove them from configuration if they exist. - for _, optionName := range []string{"distributed_plugin", "logger_plugin"} { - if _, ok := config.Options[optionName]; ok { - delete(config.Options, optionName) + if options, ok := config["options"].(map[string]interface{}); ok { + // Launcher manages plugins so remove them from configuration if they exist. + for _, optionName := range []string{"distributed_plugin", "logger_plugin"} { + delete(options, optionName) } } diff --git a/server/launcher/launcher_test.go b/server/launcher/launcher_test.go index 32dc6b5b2..932e9b6b8 100644 --- a/server/launcher/launcher_test.go +++ b/server/launcher/launcher_test.go @@ -34,7 +34,7 @@ func TestLauncherRequestConfig(t *testing.T) { require.Nil(t, err) assert.True(t, tls.AuthenticateHostFuncInvoked) assert.False(t, invalid) - assert.Equal(t, `{"options":{"key":"value"},"decorators":{}}`, config) + assert.JSONEq(t, `{"options":{"key":"value"},"decorators":{"deco":"foobar"}}`, config) } func TestLauncherRequestQueries(t *testing.T) { @@ -122,11 +122,14 @@ func newTLSService(t *testing.T) *mock.TLSService { }, GetClientConfigFunc: func( ctx context.Context, - ) (config *kolide.OsqueryConfig, err error) { - return &kolide.OsqueryConfig{ - Options: map[string]interface{}{ + ) (config map[string]interface{}, err error) { + return map[string]interface{}{ + "options": map[string]interface{}{ "key": "value", }, + "decorators": map[string]interface{}{ + "deco": "foobar", + }, }, nil }, diff --git a/server/mock/datastore.go b/server/mock/datastore.go index 88e88e4d4..0b7e97e31 100644 --- a/server/mock/datastore.go +++ b/server/mock/datastore.go @@ -4,33 +4,36 @@ package mock //go:generate mockimpl -o datastore_invites.go "s *InviteStore" "kolide.InviteStore" //go:generate mockimpl -o datastore_appconfig.go "s *AppConfigStore" "kolide.AppConfigStore" //go:generate mockimpl -o datastore_labels.go "s *LabelStore" "kolide.LabelStore" -//go:generate mockimpl -o datastore_decorators.go "s *DecoratorStore" "kolide.DecoratorStore" //go:generate mockimpl -o datastore_options.go "s *OptionStore" "kolide.OptionStore" //go:generate mockimpl -o datastore_packs.go "s *PackStore" "kolide.PackStore" //go:generate mockimpl -o datastore_hosts.go "s *HostStore" "kolide.HostStore" //go:generate mockimpl -o datastore_fim.go "s *FileIntegrityMonitoringStore" "kolide.FileIntegrityMonitoringStore" +//go:generate mockimpl -o datastore_osquery_options.go "s *OsqueryOptionsStore" "kolide.OsqueryOptionsStore" +//go:generate mockimpl -o datastore_scheduled_queries.go "s *ScheduledQueryStore" "kolide.ScheduledQueryStore" +//go:generate mockimpl -o datastore_queries.go "s *QueryStore" "kolide.QueryStore" +//go:generate mockimpl -o datastore_campaigns.go "s *CampaignStore" "kolide.CampaignStore" import "github.com/kolide/fleet/server/kolide" var _ kolide.Datastore = (*Store)(nil) type Store struct { - kolide.CampaignStore kolide.SessionStore kolide.PasswordResetStore - kolide.QueryStore - kolide.ScheduledQueryStore kolide.YARAStore kolide.TargetStore + CampaignStore + ScheduledQueryStore + OsqueryOptionsStore FileIntegrityMonitoringStore AppConfigStore - DecoratorStore HostStore InviteStore LabelStore OptionStore PackStore UserStore + QueryStore } func (m *Store) Drop() error { diff --git a/server/mock/datastore_campaigns.go b/server/mock/datastore_campaigns.go new file mode 100644 index 000000000..2f8367542 --- /dev/null +++ b/server/mock/datastore_campaigns.go @@ -0,0 +1,83 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import ( + "time" + + "github.com/kolide/fleet/server/kolide" +) + +var _ kolide.CampaignStore = (*CampaignStore)(nil) + +type NewDistributedQueryCampaignFunc func(camp *kolide.DistributedQueryCampaign) (*kolide.DistributedQueryCampaign, error) + +type DistributedQueryCampaignFunc func(id uint) (*kolide.DistributedQueryCampaign, error) + +type SaveDistributedQueryCampaignFunc func(camp *kolide.DistributedQueryCampaign) error + +type DistributedQueryCampaignTargetIDsFunc func(id uint) (hostIDs []uint, labelIDs []uint, err error) + +type NewDistributedQueryCampaignTargetFunc func(target *kolide.DistributedQueryCampaignTarget) (*kolide.DistributedQueryCampaignTarget, error) + +type NewDistributedQueryExecutionFunc func(exec *kolide.DistributedQueryExecution) (*kolide.DistributedQueryExecution, error) + +type CleanupDistributedQueryCampaignsFunc func(now time.Time) (expired uint, deleted uint, err error) + +type CampaignStore struct { + NewDistributedQueryCampaignFunc NewDistributedQueryCampaignFunc + NewDistributedQueryCampaignFuncInvoked bool + + DistributedQueryCampaignFunc DistributedQueryCampaignFunc + DistributedQueryCampaignFuncInvoked bool + + SaveDistributedQueryCampaignFunc SaveDistributedQueryCampaignFunc + SaveDistributedQueryCampaignFuncInvoked bool + + DistributedQueryCampaignTargetIDsFunc DistributedQueryCampaignTargetIDsFunc + DistributedQueryCampaignTargetIDsFuncInvoked bool + + NewDistributedQueryCampaignTargetFunc NewDistributedQueryCampaignTargetFunc + NewDistributedQueryCampaignTargetFuncInvoked bool + + NewDistributedQueryExecutionFunc NewDistributedQueryExecutionFunc + NewDistributedQueryExecutionFuncInvoked bool + + CleanupDistributedQueryCampaignsFunc CleanupDistributedQueryCampaignsFunc + CleanupDistributedQueryCampaignsFuncInvoked bool +} + +func (s *CampaignStore) NewDistributedQueryCampaign(camp *kolide.DistributedQueryCampaign) (*kolide.DistributedQueryCampaign, error) { + s.NewDistributedQueryCampaignFuncInvoked = true + return s.NewDistributedQueryCampaignFunc(camp) +} + +func (s *CampaignStore) DistributedQueryCampaign(id uint) (*kolide.DistributedQueryCampaign, error) { + s.DistributedQueryCampaignFuncInvoked = true + return s.DistributedQueryCampaignFunc(id) +} + +func (s *CampaignStore) SaveDistributedQueryCampaign(camp *kolide.DistributedQueryCampaign) error { + s.SaveDistributedQueryCampaignFuncInvoked = true + return s.SaveDistributedQueryCampaignFunc(camp) +} + +func (s *CampaignStore) DistributedQueryCampaignTargetIDs(id uint) (hostIDs []uint, labelIDs []uint, err error) { + s.DistributedQueryCampaignTargetIDsFuncInvoked = true + return s.DistributedQueryCampaignTargetIDsFunc(id) +} + +func (s *CampaignStore) NewDistributedQueryCampaignTarget(target *kolide.DistributedQueryCampaignTarget) (*kolide.DistributedQueryCampaignTarget, error) { + s.NewDistributedQueryCampaignTargetFuncInvoked = true + return s.NewDistributedQueryCampaignTargetFunc(target) +} + +func (s *CampaignStore) NewDistributedQueryExecution(exec *kolide.DistributedQueryExecution) (*kolide.DistributedQueryExecution, error) { + s.NewDistributedQueryExecutionFuncInvoked = true + return s.NewDistributedQueryExecutionFunc(exec) +} + +func (s *CampaignStore) CleanupDistributedQueryCampaigns(now time.Time) (expired uint, deleted uint, err error) { + s.CleanupDistributedQueryCampaignsFuncInvoked = true + return s.CleanupDistributedQueryCampaignsFunc(now) +} diff --git a/server/mock/datastore_decorators.go b/server/mock/datastore_decorators.go deleted file mode 100644 index deb43e010..000000000 --- a/server/mock/datastore_decorators.go +++ /dev/null @@ -1,59 +0,0 @@ -// Automatically generated by mockimpl. DO NOT EDIT! - -package mock - -import "github.com/kolide/fleet/server/kolide" - -var _ kolide.DecoratorStore = (*DecoratorStore)(nil) - -type NewDecoratorFunc func(decorator *kolide.Decorator, opts ...kolide.OptionalArg) (*kolide.Decorator, error) - -type DeleteDecoratorFunc func(id uint) error - -type DecoratorFunc func(id uint) (*kolide.Decorator, error) - -type ListDecoratorsFunc func(opts ...kolide.OptionalArg) ([]*kolide.Decorator, error) - -type SaveDecoratorFunc func(dec *kolide.Decorator, opts ...kolide.OptionalArg) error - -type DecoratorStore struct { - NewDecoratorFunc NewDecoratorFunc - NewDecoratorFuncInvoked bool - - DeleteDecoratorFunc DeleteDecoratorFunc - DeleteDecoratorFuncInvoked bool - - DecoratorFunc DecoratorFunc - DecoratorFuncInvoked bool - - ListDecoratorsFunc ListDecoratorsFunc - ListDecoratorsFuncInvoked bool - - SaveDecoratorFunc SaveDecoratorFunc - SaveDecoratorFuncInvoked bool -} - -func (s *DecoratorStore) NewDecorator(decorator *kolide.Decorator, opts ...kolide.OptionalArg) (*kolide.Decorator, error) { - s.NewDecoratorFuncInvoked = true - return s.NewDecoratorFunc(decorator, opts...) -} - -func (s *DecoratorStore) DeleteDecorator(id uint) error { - s.DeleteDecoratorFuncInvoked = true - return s.DeleteDecoratorFunc(id) -} - -func (s *DecoratorStore) Decorator(id uint) (*kolide.Decorator, error) { - s.DecoratorFuncInvoked = true - return s.DecoratorFunc(id) -} - -func (s *DecoratorStore) ListDecorators(opts ...kolide.OptionalArg) ([]*kolide.Decorator, error) { - s.ListDecoratorsFuncInvoked = true - return s.ListDecoratorsFunc(opts...) -} - -func (s *DecoratorStore) SaveDecorator(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - s.SaveDecoratorFuncInvoked = true - return s.SaveDecoratorFunc(dec, opts...) -} diff --git a/server/mock/datastore_hosts.go b/server/mock/datastore_hosts.go index 50599e34a..61db2d0dd 100644 --- a/server/mock/datastore_hosts.go +++ b/server/mock/datastore_hosts.go @@ -32,6 +32,8 @@ type GenerateHostStatusStatisticsFunc func(now time.Time) (online uint, offline type DistributedQueriesForHostFunc func(host *kolide.Host) (map[uint]string, error) +type HostIDsByNameFunc func(hostnames []string) ([]uint, error) + type HostStore struct { NewHostFunc NewHostFunc NewHostFuncInvoked bool @@ -65,6 +67,9 @@ type HostStore struct { DistributedQueriesForHostFunc DistributedQueriesForHostFunc DistributedQueriesForHostFuncInvoked bool + + HostIDsByNameFunc HostIDsByNameFunc + HostIDsByNameFuncInvoked bool } func (s *HostStore) NewHost(host *kolide.Host) (*kolide.Host, error) { @@ -121,3 +126,8 @@ func (s *HostStore) DistributedQueriesForHost(host *kolide.Host) (map[uint]strin s.DistributedQueriesForHostFuncInvoked = true return s.DistributedQueriesForHostFunc(host) } + +func (s *HostStore) HostIDsByName(hostnames []string) ([]uint, error) { + s.HostIDsByNameFuncInvoked = true + return s.HostIDsByNameFunc(hostnames) +} diff --git a/server/mock/datastore_labels.go b/server/mock/datastore_labels.go index 0711fdb5a..45e421b55 100644 --- a/server/mock/datastore_labels.go +++ b/server/mock/datastore_labels.go @@ -10,9 +10,13 @@ import ( var _ kolide.LabelStore = (*LabelStore)(nil) -type NewLabelFunc func(Label *kolide.Label, opts ...kolide.OptionalArg) (*kolide.Label, error) +type ApplyLabelSpecsFunc func(specs []*kolide.LabelSpec) error -type DeleteLabelFunc func(lid uint) error +type GetLabelSpecsFunc func() ([]*kolide.LabelSpec, error) + +type GetLabelSpecFunc func(name string) (*kolide.LabelSpec, error) + +type DeleteLabelFunc func(name string) error type LabelFunc func(lid uint) (*kolide.Label, error) @@ -30,11 +34,17 @@ type ListUniqueHostsInLabelsFunc func(labels []uint) ([]kolide.Host, error) type SearchLabelsFunc func(query string, omit ...uint) ([]kolide.Label, error) -type SaveLabelFunc func(label *kolide.Label) (*kolide.Label, error) +type LabelIDsByNameFunc func(labels []string) ([]uint, error) type LabelStore struct { - NewLabelFunc NewLabelFunc - NewLabelFuncInvoked bool + ApplyLabelSpecsFunc ApplyLabelSpecsFunc + ApplyLabelSpecsFuncInvoked bool + + GetLabelSpecsFunc GetLabelSpecsFunc + GetLabelSpecsFuncInvoked bool + + GetLabelSpecFunc GetLabelSpecFunc + GetLabelSpecFuncInvoked bool DeleteLabelFunc DeleteLabelFunc DeleteLabelFuncInvoked bool @@ -63,18 +73,28 @@ type LabelStore struct { SearchLabelsFunc SearchLabelsFunc SearchLabelsFuncInvoked bool - SaveLabelFunc SaveLabelFunc - SaveLabelFuncInvoked bool + LabelIDsByNameFunc LabelIDsByNameFunc + LabelIDsByNameFuncInvoked bool } -func (s *LabelStore) NewLabel(Label *kolide.Label, opts ...kolide.OptionalArg) (*kolide.Label, error) { - s.NewLabelFuncInvoked = true - return s.NewLabelFunc(Label, opts...) +func (s *LabelStore) ApplyLabelSpecs(specs []*kolide.LabelSpec) error { + s.ApplyLabelSpecsFuncInvoked = true + return s.ApplyLabelSpecsFunc(specs) } -func (s *LabelStore) DeleteLabel(lid uint) error { +func (s *LabelStore) GetLabelSpecs() ([]*kolide.LabelSpec, error) { + s.GetLabelSpecsFuncInvoked = true + return s.GetLabelSpecsFunc() +} + +func (s *LabelStore) GetLabelSpec(name string) (*kolide.LabelSpec, error) { + s.GetLabelSpecFuncInvoked = true + return s.GetLabelSpecFunc(name) +} + +func (s *LabelStore) DeleteLabel(name string) error { s.DeleteLabelFuncInvoked = true - return s.DeleteLabelFunc(lid) + return s.DeleteLabelFunc(name) } func (s *LabelStore) Label(lid uint) (*kolide.Label, error) { @@ -117,7 +137,7 @@ func (s *LabelStore) SearchLabels(query string, omit ...uint) ([]kolide.Label, e return s.SearchLabelsFunc(query, omit...) } -func (s *LabelStore) SaveLabel(label *kolide.Label) (*kolide.Label, error) { - s.SaveLabelFuncInvoked = true - return s.SaveLabelFunc(label) +func (s *LabelStore) LabelIDsByName(labels []string) ([]uint, error) { + s.LabelIDsByNameFuncInvoked = true + return s.LabelIDsByNameFunc(labels) } diff --git a/server/mock/datastore_osquery_options.go b/server/mock/datastore_osquery_options.go new file mode 100644 index 000000000..cb78f24cd --- /dev/null +++ b/server/mock/datastore_osquery_options.go @@ -0,0 +1,43 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import ( + "encoding/json" + + "github.com/kolide/fleet/server/kolide" +) + +var _ kolide.OsqueryOptionsStore = (*OsqueryOptionsStore)(nil) + +type ApplyOptionsFunc func(options *kolide.OptionsSpec) error + +type GetOptionsFunc func() (*kolide.OptionsSpec, error) + +type OptionsForPlatformFunc func(platform string) (json.RawMessage, error) + +type OsqueryOptionsStore struct { + ApplyOptionsFunc ApplyOptionsFunc + ApplyOptionsFuncInvoked bool + + GetOptionsFunc GetOptionsFunc + GetOptionsFuncInvoked bool + + OptionsForPlatformFunc OptionsForPlatformFunc + OptionsForPlatformFuncInvoked bool +} + +func (s *OsqueryOptionsStore) ApplyOptions(options *kolide.OptionsSpec) error { + s.ApplyOptionsFuncInvoked = true + return s.ApplyOptionsFunc(options) +} + +func (s *OsqueryOptionsStore) GetOptions() (*kolide.OptionsSpec, error) { + s.GetOptionsFuncInvoked = true + return s.GetOptionsFunc() +} + +func (s *OsqueryOptionsStore) OptionsForPlatform(platform string) (json.RawMessage, error) { + s.OptionsForPlatformFuncInvoked = true + return s.OptionsForPlatformFunc(platform) +} diff --git a/server/mock/datastore_packs.go b/server/mock/datastore_packs.go index 24bb6c3a4..87f34bcd8 100644 --- a/server/mock/datastore_packs.go +++ b/server/mock/datastore_packs.go @@ -6,11 +6,13 @@ import "github.com/kolide/fleet/server/kolide" var _ kolide.PackStore = (*PackStore)(nil) -type NewPackFunc func(pack *kolide.Pack, opts ...kolide.OptionalArg) (*kolide.Pack, error) +type ApplyPackSpecsFunc func(specs []*kolide.PackSpec) error -type SavePackFunc func(pack *kolide.Pack) error +type GetPackSpecsFunc func() ([]*kolide.PackSpec, error) -type DeletePackFunc func(pid uint) error +type GetPackSpecFunc func(name string) (*kolide.PackSpec, error) + +type DeletePackFunc func(name string) error type PackFunc func(pid uint) (*kolide.Pack, error) @@ -18,26 +20,23 @@ type ListPacksFunc func(opt kolide.ListOptions) ([]*kolide.Pack, error) type PackByNameFunc func(name string, opts ...kolide.OptionalArg) (*kolide.Pack, bool, error) -type AddLabelToPackFunc func(lid uint, pid uint, opts ...kolide.OptionalArg) error - -type RemoveLabelFromPackFunc func(lid uint, pid uint) error - type ListLabelsForPackFunc func(pid uint) ([]*kolide.Label, error) -type AddHostToPackFunc func(hid uint, pid uint) error - -type RemoveHostFromPackFunc func(hid uint, pid uint) error +type ListPacksForHostFunc func(hid uint) (packs []*kolide.Pack, err error) type ListHostsInPackFunc func(pid uint, opt kolide.ListOptions) ([]uint, error) type ListExplicitHostsInPackFunc func(pid uint, opt kolide.ListOptions) ([]uint, error) type PackStore struct { - NewPackFunc NewPackFunc - NewPackFuncInvoked bool + ApplyPackSpecsFunc ApplyPackSpecsFunc + ApplyPackSpecsFuncInvoked bool - SavePackFunc SavePackFunc - SavePackFuncInvoked bool + GetPackSpecsFunc GetPackSpecsFunc + GetPackSpecsFuncInvoked bool + + GetPackSpecFunc GetPackSpecFunc + GetPackSpecFuncInvoked bool DeletePackFunc DeletePackFunc DeletePackFuncInvoked bool @@ -51,20 +50,11 @@ type PackStore struct { PackByNameFunc PackByNameFunc PackByNameFuncInvoked bool - AddLabelToPackFunc AddLabelToPackFunc - AddLabelToPackFuncInvoked bool - - RemoveLabelFromPackFunc RemoveLabelFromPackFunc - RemoveLabelFromPackFuncInvoked bool - ListLabelsForPackFunc ListLabelsForPackFunc ListLabelsForPackFuncInvoked bool - AddHostToPackFunc AddHostToPackFunc - AddHostToPackFuncInvoked bool - - RemoveHostFromPackFunc RemoveHostFromPackFunc - RemoveHostFromPackFuncInvoked bool + ListPacksForHostFunc ListPacksForHostFunc + ListPacksForHostFuncInvoked bool ListHostsInPackFunc ListHostsInPackFunc ListHostsInPackFuncInvoked bool @@ -73,19 +63,24 @@ type PackStore struct { ListExplicitHostsInPackFuncInvoked bool } -func (s *PackStore) NewPack(pack *kolide.Pack, opts ...kolide.OptionalArg) (*kolide.Pack, error) { - s.NewPackFuncInvoked = true - return s.NewPackFunc(pack, opts...) +func (s *PackStore) ApplyPackSpecs(specs []*kolide.PackSpec) error { + s.ApplyPackSpecsFuncInvoked = true + return s.ApplyPackSpecsFunc(specs) } -func (s *PackStore) SavePack(pack *kolide.Pack) error { - s.SavePackFuncInvoked = true - return s.SavePackFunc(pack) +func (s *PackStore) GetPackSpecs() ([]*kolide.PackSpec, error) { + s.GetPackSpecsFuncInvoked = true + return s.GetPackSpecsFunc() } -func (s *PackStore) DeletePack(pid uint) error { +func (s *PackStore) GetPackSpec(name string) (*kolide.PackSpec, error) { + s.GetPackSpecFuncInvoked = true + return s.GetPackSpecFunc(name) +} + +func (s *PackStore) DeletePack(name string) error { s.DeletePackFuncInvoked = true - return s.DeletePackFunc(pid) + return s.DeletePackFunc(name) } func (s *PackStore) Pack(pid uint) (*kolide.Pack, error) { @@ -103,29 +98,14 @@ func (s *PackStore) PackByName(name string, opts ...kolide.OptionalArg) (*kolide return s.PackByNameFunc(name, opts...) } -func (s *PackStore) AddLabelToPack(lid uint, pid uint, opts ...kolide.OptionalArg) error { - s.AddLabelToPackFuncInvoked = true - return s.AddLabelToPackFunc(lid, pid, opts...) -} - -func (s *PackStore) RemoveLabelFromPack(lid uint, pid uint) error { - s.RemoveLabelFromPackFuncInvoked = true - return s.RemoveLabelFromPackFunc(lid, pid) -} - func (s *PackStore) ListLabelsForPack(pid uint) ([]*kolide.Label, error) { s.ListLabelsForPackFuncInvoked = true return s.ListLabelsForPackFunc(pid) } -func (s *PackStore) AddHostToPack(hid uint, pid uint) error { - s.AddHostToPackFuncInvoked = true - return s.AddHostToPackFunc(hid, pid) -} - -func (s *PackStore) RemoveHostFromPack(hid uint, pid uint) error { - s.RemoveHostFromPackFuncInvoked = true - return s.RemoveHostFromPackFunc(hid, pid) +func (s *PackStore) ListPacksForHost(hid uint) (packs []*kolide.Pack, err error) { + s.ListPacksForHostFuncInvoked = true + return s.ListPacksForHostFunc(hid) } func (s *PackStore) ListHostsInPack(pid uint, opt kolide.ListOptions) ([]uint, error) { diff --git a/server/mock/datastore_queries.go b/server/mock/datastore_queries.go new file mode 100644 index 000000000..53bced7e2 --- /dev/null +++ b/server/mock/datastore_queries.go @@ -0,0 +1,89 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import "github.com/kolide/fleet/server/kolide" + +var _ kolide.QueryStore = (*QueryStore)(nil) + +type ApplyQueriesFunc func(authorID uint, queries []*kolide.Query) error + +type NewQueryFunc func(query *kolide.Query, opts ...kolide.OptionalArg) (*kolide.Query, error) + +type SaveQueryFunc func(query *kolide.Query) error + +type DeleteQueryFunc func(name string) error + +type DeleteQueriesFunc func(ids []uint) (uint, error) + +type QueryFunc func(id uint) (*kolide.Query, error) + +type ListQueriesFunc func(opt kolide.ListOptions) ([]*kolide.Query, error) + +type QueryByNameFunc func(name string, opts ...kolide.OptionalArg) (*kolide.Query, error) + +type QueryStore struct { + ApplyQueriesFunc ApplyQueriesFunc + ApplyQueriesFuncInvoked bool + + NewQueryFunc NewQueryFunc + NewQueryFuncInvoked bool + + SaveQueryFunc SaveQueryFunc + SaveQueryFuncInvoked bool + + DeleteQueryFunc DeleteQueryFunc + DeleteQueryFuncInvoked bool + + DeleteQueriesFunc DeleteQueriesFunc + DeleteQueriesFuncInvoked bool + + QueryFunc QueryFunc + QueryFuncInvoked bool + + ListQueriesFunc ListQueriesFunc + ListQueriesFuncInvoked bool + + QueryByNameFunc QueryByNameFunc + QueryByNameFuncInvoked bool +} + +func (s *QueryStore) ApplyQueries(authorID uint, queries []*kolide.Query) error { + s.ApplyQueriesFuncInvoked = true + return s.ApplyQueriesFunc(authorID, queries) +} + +func (s *QueryStore) NewQuery(query *kolide.Query, opts ...kolide.OptionalArg) (*kolide.Query, error) { + s.NewQueryFuncInvoked = true + return s.NewQueryFunc(query, opts...) +} + +func (s *QueryStore) SaveQuery(query *kolide.Query) error { + s.SaveQueryFuncInvoked = true + return s.SaveQueryFunc(query) +} + +func (s *QueryStore) DeleteQuery(name string) error { + s.DeleteQueryFuncInvoked = true + return s.DeleteQueryFunc(name) +} + +func (s *QueryStore) DeleteQueries(ids []uint) (uint, error) { + s.DeleteQueriesFuncInvoked = true + return s.DeleteQueriesFunc(ids) +} + +func (s *QueryStore) Query(id uint) (*kolide.Query, error) { + s.QueryFuncInvoked = true + return s.QueryFunc(id) +} + +func (s *QueryStore) ListQueries(opt kolide.ListOptions) ([]*kolide.Query, error) { + s.ListQueriesFuncInvoked = true + return s.ListQueriesFunc(opt) +} + +func (s *QueryStore) QueryByName(name string, opts ...kolide.OptionalArg) (*kolide.Query, error) { + s.QueryByNameFuncInvoked = true + return s.QueryByNameFunc(name, opts...) +} diff --git a/server/mock/datastore_scheduled_queries.go b/server/mock/datastore_scheduled_queries.go new file mode 100644 index 000000000..1c6ad685b --- /dev/null +++ b/server/mock/datastore_scheduled_queries.go @@ -0,0 +1,19 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import "github.com/kolide/fleet/server/kolide" + +var _ kolide.ScheduledQueryStore = (*ScheduledQueryStore)(nil) + +type ListScheduledQueriesInPackFunc func(id uint, opts kolide.ListOptions) ([]*kolide.ScheduledQuery, error) + +type ScheduledQueryStore struct { + ListScheduledQueriesInPackFunc ListScheduledQueriesInPackFunc + ListScheduledQueriesInPackFuncInvoked bool +} + +func (s *ScheduledQueryStore) ListScheduledQueriesInPack(id uint, opts kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { + s.ListScheduledQueriesInPackFuncInvoked = true + return s.ListScheduledQueriesInPackFunc(id, opts) +} diff --git a/server/mock/service_osquery.go b/server/mock/service_osquery.go index f0458be0e..69860c228 100644 --- a/server/mock/service_osquery.go +++ b/server/mock/service_osquery.go @@ -15,7 +15,7 @@ type EnrollAgentFunc func(ctx context.Context, enrollSecret string, hostIdentifi type AuthenticateHostFuncI func(ctx context.Context, nodeKey string) (host *kolide.Host, err error) -type GetClientConfigFunc func(ctx context.Context) (config *kolide.OsqueryConfig, err error) +type GetClientConfigFunc func(ctx context.Context) (config map[string]interface{}, err error) type GetDistributedQueriesFunc func(ctx context.Context) (queries map[string]string, accelerate uint, err error) @@ -58,7 +58,7 @@ func (s *TLSService) AuthenticateHost(ctx context.Context, nodeKey string) (host return s.AuthenticateHostFunc(ctx, nodeKey) } -func (s *TLSService) GetClientConfig(ctx context.Context) (config *kolide.OsqueryConfig, err error) { +func (s *TLSService) GetClientConfig(ctx context.Context) (config map[string]interface{}, err error) { s.GetClientConfigFuncInvoked = true return s.GetClientConfigFunc(ctx) } diff --git a/server/service/client.go b/server/service/client.go new file mode 100644 index 000000000..644201b42 --- /dev/null +++ b/server/service/client.go @@ -0,0 +1,103 @@ +package service + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" +) + +type Client struct { + addr string + baseURL *url.URL + token string + http *http.Client + insecureSkipVerify bool +} + +func NewClient(addr string, insecureSkipVerify bool) (*Client, error) { + if !strings.HasPrefix(addr, "https://") { + return nil, errors.New("Addrress must start with https://") + } + + baseURL, err := url.Parse(addr) + if err != nil { + return nil, errors.Wrap(err, "parsing URL") + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}, + }, + } + + return &Client{ + addr: addr, + baseURL: baseURL, + http: httpClient, + insecureSkipVerify: insecureSkipVerify, + }, nil +} + +func (c *Client) doWithHeaders(verb, path string, params interface{}, headers map[string]string) (*http.Response, error) { + var bodyBytes []byte + var err error + if params != nil { + bodyBytes, err = json.Marshal(params) + if err != nil { + return nil, errors.Wrap(err, "marshaling json") + } + } + + request, err := http.NewRequest( + verb, + c.url(path).String(), + bytes.NewBuffer(bodyBytes), + ) + if err != nil { + return nil, errors.Wrap(err, "creating request object") + } + for k, v := range headers { + request.Header.Set(k, v) + } + + return c.http.Do(request) +} + +func (c *Client) Do(verb, path string, params interface{}) (*http.Response, error) { + headers := map[string]string{ + "Content-type": "application/json", + "Accept": "application/json", + } + + return c.doWithHeaders(verb, path, params, headers) +} + +func (c *Client) AuthenticatedDo(verb, path string, params interface{}) (*http.Response, error) { + if c.token == "" { + return nil, errors.New("authentication token is empty") + } + + headers := map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", c.token), + } + + return c.doWithHeaders(verb, path, params, headers) +} + +func (c *Client) SetToken(t string) { + c.token = t +} + +func (c *Client) url(path string) *url.URL { + u := *c.baseURL + u.Path = path + return &u +} diff --git a/server/service/client_errors.go b/server/service/client_errors.go new file mode 100644 index 000000000..2debc10a5 --- /dev/null +++ b/server/service/client_errors.go @@ -0,0 +1,88 @@ +package service + +import ( + "encoding/json" + "io" +) + +type SetupAlreadyErr interface { + SetupAlready() bool + Error() string +} + +type setupAlreadyErr struct{} + +func (e setupAlreadyErr) Error() string { + return "Kolide Fleet has already been setup" +} + +func (e setupAlreadyErr) SetupAlready() bool { + return true +} + +type InvalidLoginErr interface { + InvalidLogin() bool + Error() string +} + +type invalidLoginErr struct{} + +func (e invalidLoginErr) Error() string { + return "The credentials supplied were invalid" +} + +func (e invalidLoginErr) InvalidLogin() bool { + return true +} + +type NotSetupErr interface { + NotSetup() bool + Error() string +} + +type notSetupErr struct{} + +func (e notSetupErr) Error() string { + return "The Kolide Fleet instance is not set up yet" +} + +func (e notSetupErr) NotSetup() bool { + return true +} + +type NotFoundErr interface { + NotFound() bool + Error() string +} + +type notFoundErr struct{} + +func (e notFoundErr) Error() string { + return "The resource was not found" +} + +func (n notFoundErr) NotFound() bool { + return true +} + +type serverError struct { + Message string `json:"message"` + Errors []struct { + Name string `json:"name"` + Reason string `json:"reason"` + } `json:"errors"` +} + +func extractServerErrorText(body io.Reader) string { + var serverErr serverError + if err := json.NewDecoder(body).Decode(&serverErr); err != nil { + return "unknown" + } + + errText := serverErr.Message + if len(serverErr.Errors) > 0 { + errText += ": " + serverErr.Errors[0].Reason + } + + return errText +} diff --git a/server/service/client_labels.go b/server/service/client_labels.go new file mode 100644 index 000000000..4cdba3618 --- /dev/null +++ b/server/service/client_labels.go @@ -0,0 +1,138 @@ +package service + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// ApplyLabels sends the list of Labels to be applied (upserted) to the +// 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) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/spec/labels") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "apply labels received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody applyLabelSpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode apply label spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("apply label spec: %s", responseBody.Err) + } + + return nil +} + +// 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.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/labels") + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return nil, notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get label received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getLabelSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get label spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get label spec: %s", responseBody.Err) + } + + return responseBody.Spec, nil +} + +// 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) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/labels") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get labels received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getLabelSpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get label spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get label spec: %s", responseBody.Err) + } + + return responseBody.Specs, nil +} + +// DeleteLabel deletes the label with the matching name. +func (c *Client) DeleteLabel(name string) error { + verb, path := "DELETE", "/api/v1/kolide/labels/"+url.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return errors.Wrapf(err, "%s %s", verb, path) + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "delete label received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody deleteLabelResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode get label spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("get label spec: %s", responseBody.Err) + } + + return nil +} diff --git a/server/service/client_live_query.go b/server/service/client_live_query.go new file mode 100644 index 000000000..a295d706c --- /dev/null +++ b/server/service/client_live_query.go @@ -0,0 +1,159 @@ +package service + +import ( + "crypto/tls" + "encoding/json" + "net/http" + "sync/atomic" + "time" + + "github.com/kolide/fleet/server/kolide" + ws "github.com/kolide/fleet/server/websocket" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +// LiveQueryResultsHandler provides access to all of the information about an +// incoming stream of live query results. +type LiveQueryResultsHandler struct { + errors chan error + results chan kolide.DistributedQueryResult + totals atomic.Value // real type: targetTotals + status atomic.Value // real type: campaignStatus +} + +func NewLiveQueryResultsHandler() *LiveQueryResultsHandler { + return &LiveQueryResultsHandler{ + errors: make(chan error), + results: make(chan kolide.DistributedQueryResult), + } +} + +// Errors returns a read channel that includes any errors returned by the +// server or receiving the results. +func (h *LiveQueryResultsHandler) Errors() <-chan error { + return h.errors +} + +// Results returns a read channel including any received results +func (h *LiveQueryResultsHandler) Results() <-chan kolide.DistributedQueryResult { + return h.results +} + +// Totals returns the current metadata of hosts targeted by the query +func (h *LiveQueryResultsHandler) Totals() *targetTotals { + return h.totals.Load().(*targetTotals) +} + +func (h *LiveQueryResultsHandler) Status() *campaignStatus { + s := h.status.Load() + if s != nil { + return s.(*campaignStatus) + } + return nil +} + +// LiveQuery creates a new live query and begins streaming results. +func (c *Client) LiveQuery(query string, labels []string, hosts []string) (*LiveQueryResultsHandler, error) { + req := createDistributedQueryCampaignByNamesRequest{ + Query: query, + Selected: distributedQueryCampaignTargetsByNames{Labels: labels, Hosts: hosts}, + } + 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") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "create live query received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody createDistributedQueryCampaignResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode create live query response") + } + if responseBody.Err != nil { + return nil, errors.Errorf("create live query: %s", responseBody.Err) + } + + // Copy default dialer but skip cert verification if set. + dialer := &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: c.insecureSkipVerify}, + } + + wssURL := *c.baseURL + wssURL.Scheme = "wss" + wssURL.Path = "/api/v1/kolide/results/websocket" + conn, _, err := dialer.Dial(wssURL.String(), nil) + if err != nil { + return nil, errors.Wrap(err, "upgrade live query result websocket") + } + + err = conn.WriteJSON(ws.JSONMessage{ + Type: "auth", + Data: map[string]interface{}{"token": c.token}, + }) + if err != nil { + return nil, errors.Wrap(err, "auth for results") + } + + err = conn.WriteJSON(ws.JSONMessage{ + Type: "select_campaign", + Data: map[string]interface{}{"campaign_id": responseBody.Campaign.ID}, + }) + if err != nil { + return nil, errors.Wrap(err, "auth for results") + } + + resHandler := NewLiveQueryResultsHandler() + go func() { + defer conn.Close() + for { + msg := struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` + }{} + err := conn.ReadJSON(&msg) + if err != nil { + resHandler.errors <- errors.Wrap(err, "receive ws message") + } + + switch msg.Type { + case "result": + var res kolide.DistributedQueryResult + if err := json.Unmarshal(msg.Data, &res); err != nil { + resHandler.errors <- errors.Wrap(err, "unmarshal results") + } + resHandler.results <- res + + case "totals": + var totals targetTotals + if err := json.Unmarshal(msg.Data, &totals); err != nil { + resHandler.errors <- errors.Wrap(err, "unmarshal totals") + } + resHandler.totals.Store(&totals) + + case "status": + var status campaignStatus + if err := json.Unmarshal(msg.Data, &status); err != nil { + resHandler.errors <- errors.Wrap(err, "unmarshal status") + } + resHandler.status.Store(&status) + + default: + resHandler.errors <- errors.Errorf("unknown msg type %s", msg.Type) + } + } + }() + + return resHandler, nil +} diff --git a/server/service/client_options.go b/server/service/client_options.go new file mode 100644 index 000000000..ab561a471 --- /dev/null +++ b/server/service/client_options.go @@ -0,0 +1,73 @@ +package service + +import ( + "encoding/json" + "net/http" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// 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) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/spec/osquery_options") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "apply options received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody applyOsqueryOptionsSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode apply options spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("apply options spec: %s", responseBody.Err) + } + + return nil +} + +// 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) + if err != nil { + return nil, errors.Wrap(err, verb+" "+path) + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return nil, notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get options received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getOsqueryOptionsSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get options spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get options spec: %s", responseBody.Err) + } + + return responseBody.Spec, nil +} diff --git a/server/service/client_packs.go b/server/service/client_packs.go new file mode 100644 index 000000000..35791f9ae --- /dev/null +++ b/server/service/client_packs.go @@ -0,0 +1,138 @@ +package service + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// ApplyPacks sends the list of Packs to be applied (upserted) to the +// 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) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/spec/packs") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "apply packs received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody applyPackSpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode apply pack spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("apply pack spec: %s", responseBody.Err) + } + + return nil +} + +// GetPack retrieves information about a pack +func (c *Client) GetPack(name string) (*kolide.PackSpec, error) { + verb, path := "GET", "/api/v1/kolide/spec/packs/"+url.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/packs") + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return nil, notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get pack received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getPackSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get pack spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get pack spec: %s", responseBody.Err) + } + + return responseBody.Spec, nil +} + +// 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) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/packs") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get packs received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getPackSpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get pack spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get pack spec: %s", responseBody.Err) + } + + return responseBody.Specs, nil +} + +// DeletePack deletes the pack with the matching name. +func (c *Client) DeletePack(name string) error { + verb, path := "DELETE", "/api/v1/kolide/packs/"+url.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return errors.Wrapf(err, "%s %s", verb, path) + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "delete pack received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody deletePackResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode get pack spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("get pack spec: %s", responseBody.Err) + } + + return nil +} diff --git a/server/service/client_queries.go b/server/service/client_queries.go new file mode 100644 index 000000000..e72d7f45c --- /dev/null +++ b/server/service/client_queries.go @@ -0,0 +1,138 @@ +package service + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// ApplyQueries sends the list of Queries to be applied (upserted) to the +// 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) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/spec/queries") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "apply queries received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody applyQuerySpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode apply query spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("apply query spec: %s", responseBody.Err) + } + + return nil +} + +// 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.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return nil, errors.Wrapf(err, "%s %s", verb, path) + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return nil, notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get query received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getQuerySpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get query spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get query spec: %s", responseBody.Err) + } + + return responseBody.Spec, nil +} + +// 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) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/queries") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get queries received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getQuerySpecsResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get query spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get query spec: %s", responseBody.Err) + } + + return responseBody.Specs, nil +} + +// DeleteQuery deletes the query with the matching name. +func (c *Client) DeleteQuery(name string) error { + verb, path := "DELETE", "/api/v1/kolide/queries/"+url.QueryEscape(name) + response, err := c.AuthenticatedDo(verb, path, nil) + if err != nil { + return errors.Wrapf(err, "%s %s", verb, path) + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return notFoundErr{} + } + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "delete query received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody deleteQueryResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode get query spec response") + } + + if responseBody.Err != nil { + return errors.Errorf("get query spec: %s", responseBody.Err) + } + + return nil +} diff --git a/server/service/client_sessions.go b/server/service/client_sessions.go new file mode 100644 index 000000000..d4b52d64a --- /dev/null +++ b/server/service/client_sessions.go @@ -0,0 +1,78 @@ +package service + +import ( + "encoding/json" + "net/http" + + "github.com/pkg/errors" +) + +// Login attempts to login to the current Fleet instance. If login is successful, +// an auth token is returned. +func (c *Client) Login(email, password string) (string, error) { + params := loginRequest{ + Username: email, + Password: password, + } + + response, err := c.Do("POST", "/api/v1/kolide/login", params) + if err != nil { + return "", errors.Wrap(err, "POST /api/v1/kolide/login") + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusNotFound: + return "", notSetupErr{} + case http.StatusUnauthorized: + return "", invalidLoginErr{} + } + if response.StatusCode != http.StatusOK { + return "", errors.Errorf( + "login received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody loginResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return "", errors.Wrap(err, "decode login response") + } + + if responseBody.Err != nil { + return "", errors.Errorf("login: %s", responseBody.Err) + } + + return responseBody.Token, nil +} + +// Logout attempts to logout to the current Fleet instance. +func (c *Client) Logout() error { + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/logout", nil) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/logout") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "logout received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody logoutResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode logout response") + } + + if responseBody.Err != nil { + return errors.Errorf("logout: %s", responseBody.Err) + } + + return nil +} diff --git a/server/service/client_setup.go b/server/service/client_setup.go new file mode 100644 index 000000000..f6e316658 --- /dev/null +++ b/server/service/client_setup.go @@ -0,0 +1,62 @@ +package service + +import ( + "encoding/json" + "net/http" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +// Setup attempts to setup the current Fleet instance. If setup is successful, +// an auth token is returned. +func (c *Client) Setup(email, password, org string) (string, error) { + t := true + params := setupRequest{ + Admin: &kolide.UserPayload{ + Admin: &t, + Username: &email, + Email: &email, + Password: &password, + }, + OrgInfo: &kolide.OrgInfo{ + OrgName: &org, + }, + KolideServerURL: &c.addr, + } + + response, err := c.Do("POST", "/api/v1/setup", params) + if err != nil { + return "", errors.Wrap(err, "POST /api/v1/setup") + } + defer response.Body.Close() + + // If setup has already been completed, Kolide Fleet will not serve the + // setup route, which will cause the request to 404 + if response.StatusCode == http.StatusNotFound { + return "", setupAlreadyErr{} + } + if response.StatusCode != http.StatusOK { + return "", errors.Errorf( + "setup received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + if response.StatusCode != http.StatusOK { + return "", errors.Errorf("setup got HTTP %d, expected 200", response.StatusCode) + } + + var responseBody setupResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return "", errors.Wrap(err, "decode setup response") + } + + if responseBody.Err != nil { + return "", errors.Errorf("setup: %s", responseBody.Err) + } + + return *responseBody.Token, nil +} diff --git a/server/service/endpoint_appconfig.go b/server/service/endpoint_appconfig.go index 90b2ce063..0776f22e7 100644 --- a/server/service/endpoint_appconfig.go +++ b/server/service/endpoint_appconfig.go @@ -2,7 +2,7 @@ package service import ( "context" - "fmt" + "errors" "github.com/go-kit/kit/endpoint" "github.com/kolide/fleet/server/contexts/viewer" @@ -27,7 +27,7 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { vc, ok := viewer.FromContext(ctx) if !ok { - return nil, fmt.Errorf("could not fetch user") + return nil, errors.New("could not fetch user") } config, err := svc.AppConfig(ctx) if err != nil { diff --git a/server/service/endpoint_campaigns.go b/server/service/endpoint_campaigns.go index 4cbb672cc..9e92f4176 100644 --- a/server/service/endpoint_campaigns.go +++ b/server/service/endpoint_campaigns.go @@ -18,11 +18,13 @@ import ( //////////////////////////////////////////////////////////////////////////////// type createDistributedQueryCampaignRequest struct { - Query string `json:"query"` - Selected struct { - Labels []uint `json:"labels"` - Hosts []uint `json:"hosts"` - } `json:"selected"` + Query string `json:"query"` + Selected distributedQueryCampaignTargets `json:"selected"` +} + +type distributedQueryCampaignTargets struct { + Labels []uint `json:"labels"` + Hosts []uint `json:"hosts"` } type createDistributedQueryCampaignResponse struct { @@ -43,6 +45,31 @@ func makeCreateDistributedQueryCampaignEndpoint(svc kolide.Service) endpoint.End } } +//////////////////////////////////////////////////////////////////////////////// +// Create Distributed Query Campaign By Names +//////////////////////////////////////////////////////////////////////////////// + +type createDistributedQueryCampaignByNamesRequest struct { + Query string `json:"query"` + Selected distributedQueryCampaignTargetsByNames `json:"selected"` +} + +type distributedQueryCampaignTargetsByNames struct { + Labels []string `json:"labels"` + Hosts []string `json:"hosts"` +} + +func makeCreateDistributedQueryCampaignByNamesEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createDistributedQueryCampaignByNamesRequest) + campaign, err := svc.NewDistributedQueryCampaignByNames(ctx, req.Query, req.Selected.Hosts, req.Selected.Labels) + if err != nil { + return createQueryResponse{Err: err}, nil + } + return createDistributedQueryCampaignResponse{campaign, nil}, nil + } +} + //////////////////////////////////////////////////////////////////////////////// // Stream Distributed Query Campaign Results and Metadata //////////////////////////////////////////////////////////////////////////////// @@ -50,6 +77,7 @@ func makeCreateDistributedQueryCampaignEndpoint(svc kolide.Service) endpoint.End func makeStreamDistributedQueryCampaignResultsHandler(svc kolide.Service, jwtKey string, logger kitlog.Logger) http.Handler { opt := sockjs.DefaultOptions opt.Websocket = true + opt.RawWebsocket = true return sockjs.NewHandler("/api/v1/kolide/results", opt, func(session sockjs.Session) { defer session.Close(0, "none") diff --git a/server/service/endpoint_decorators.go b/server/service/endpoint_decorators.go deleted file mode 100644 index a5d8dad7c..000000000 --- a/server/service/endpoint_decorators.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "context" - - "github.com/go-kit/kit/endpoint" - "github.com/kolide/fleet/server/kolide" -) - -type listDecoratorResponse struct { - Decorators []*kolide.Decorator `json:"decorators"` - Err error `json:"error,omitempty"` -} - -func (r listDecoratorResponse) error() error { return r.Err } - -func makeListDecoratorsEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - decs, err := svc.ListDecorators(ctx) - if err != nil { - return listDecoratorResponse{Err: err}, nil - } - return listDecoratorResponse{Decorators: decs}, nil - } -} - -type newDecoratorRequest struct { - Payload kolide.DecoratorPayload `json:"payload"` -} - -type decoratorResponse struct { - Decorator *kolide.Decorator `json:"decorator,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r decoratorResponse) error() error { return r.Err } - -func makeNewDecoratorEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(newDecoratorRequest) - dec, err := svc.NewDecorator(ctx, r.Payload) - if err != nil { - return decoratorResponse{Err: err}, nil - } - return decoratorResponse{Decorator: dec}, nil - } -} - -type deleteDecoratorRequest struct { - ID uint -} - -type deleteDecoratorResponse struct { - Err error `json:"error,omitempty"` -} - -func (r deleteDecoratorResponse) error() error { return r.Err } - -func makeDeleteDecoratorEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(deleteDecoratorRequest) - err := svc.DeleteDecorator(ctx, r.ID) - - if err != nil { - return deleteDecoratorResponse{Err: err}, nil - } - return deleteDecoratorResponse{}, nil - } -} - -func makeModifyDecoratorEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(newDecoratorRequest) - dec, err := svc.ModifyDecorator(ctx, r.Payload) - if err != nil { - return decoratorResponse{Err: err}, nil - } - return decoratorResponse{Decorator: dec}, nil - - } -} diff --git a/server/service/endpoint_decorators_test.go b/server/service/endpoint_decorators_test.go deleted file mode 100644 index ab156f5ab..000000000 --- a/server/service/endpoint_decorators_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/kolide/fleet/server/kolide" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupDecoratorTest(r *testResource) { - decs := []kolide.Decorator{ - kolide.Decorator{ - Type: kolide.DecoratorLoad, - Query: "select something from foo;", - }, - kolide.Decorator{ - Type: kolide.DecoratorLoad, - Query: "select bar from foo;", - }, - kolide.Decorator{ - Type: kolide.DecoratorAlways, - Query: "select x from y;", - }, - kolide.Decorator{ - Type: kolide.DecoratorInterval, - Query: "select name from system_info;", - Interval: 3600, - }, - } - for _, d := range decs { - r.ds.NewDecorator(&d) - } -} - -func testModifyDecorator(t *testing.T, r *testResource) { - dec := &kolide.Decorator{ - Name: "foo", - Type: kolide.DecoratorLoad, - Query: "select foo from bar;", - } - dec, err := r.ds.NewDecorator(dec) - require.Nil(t, err) - buffer := bytes.NewBufferString(`{ - "payload": { - "type": "always", - "name": "bar", - "query": "select baz from boom;" - } - }`) - req, err := http.NewRequest("PATCH", r.server.URL+fmt.Sprintf("/api/v1/kolide/decorators/%d", dec.ID), buffer) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - - var decResp decoratorResponse - err = json.NewDecoder(resp.Body).Decode(&decResp) - require.Nil(t, err) - require.NotNil(t, decResp.Decorator) - assert.Equal(t, "select baz from boom;", decResp.Decorator.Query) - assert.Equal(t, kolide.DecoratorAlways, decResp.Decorator.Type) - assert.Equal(t, "bar", decResp.Decorator.Name) -} - -// This test verifies that we can submit the same payload twice without -// raising an error -func testModifyDecoratorNoChanges(t *testing.T, r *testResource) { - dec := &kolide.Decorator{ - Type: kolide.DecoratorLoad, - Query: "select foo from bar;", - } - dec, err := r.ds.NewDecorator(dec) - require.Nil(t, err) - buffer := bytes.NewBufferString(`{ - "payload": { - "type": "load", - "query": "select foo from bar;" - } - }`) - req, err := http.NewRequest("PATCH", r.server.URL+fmt.Sprintf("/api/v1/kolide/decorators/%d", dec.ID), buffer) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - - var decResp decoratorResponse - err = json.NewDecoder(resp.Body).Decode(&decResp) - require.Nil(t, err) - require.NotNil(t, decResp.Decorator) - assert.Equal(t, "select foo from bar;", decResp.Decorator.Query) - assert.Equal(t, kolide.DecoratorLoad, decResp.Decorator.Type) -} - -func testListDecorator(t *testing.T, r *testResource) { - setupDecoratorTest(r) - req, err := http.NewRequest("GET", r.server.URL+"/api/v1/kolide/decorators", nil) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - - var decs listDecoratorResponse - err = json.NewDecoder(resp.Body).Decode(&decs) - require.Nil(t, err) - - assert.Len(t, decs.Decorators, 4) -} - -func testNewDecorator(t *testing.T, r *testResource) { - buffer := bytes.NewBufferString( - `{ - "payload": { - "type": "load", - "query": "select x from y;" - } - }`) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/decorators", buffer) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - - var dec decoratorResponse - err = json.NewDecoder(resp.Body).Decode(&dec) - require.Nil(t, err) - require.NotNil(t, dec.Decorator) - assert.Equal(t, kolide.DecoratorLoad, dec.Decorator.Type) - assert.Equal(t, "select x from y;", dec.Decorator.Query) -} - -// invalid json -func testNewDecoratorFailType(t *testing.T, r *testResource) { - buffer := bytes.NewBufferString( - `{ - "payload": { - "type": "zip", - "query": "select x from y;" - } - }`) - - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/decorators", buffer) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - - var errStruct mockValidationError - err = json.NewDecoder(resp.Body).Decode(&errStruct) - require.Nil(t, err) - require.Len(t, errStruct.Errors, 1) - assert.Equal(t, "invalid value, must be load, always, or interval", errStruct.Errors[0].Reason) -} - -func testNewDecoratorFailValidation(t *testing.T, r *testResource) { - buffer := bytes.NewBufferString( - `{ - "payload": { - "type": "interval", - "query": "select x from y;", - "interval": 3601 - } - }`) - - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/decorators", buffer) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - - var errStruct mockValidationError - err = json.NewDecoder(resp.Body).Decode(&errStruct) - require.Nil(t, err) - require.Len(t, errStruct.Errors, 1) - assert.Equal(t, "must be divisible by 60", errStruct.Errors[0].Reason) -} - -func testDeleteDecorator(t *testing.T, r *testResource) { - setupDecoratorTest(r) - req, err := http.NewRequest("DELETE", r.server.URL+"/api/v1/kolide/decorators/1", nil) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - decs, _ := r.ds.ListDecorators() - assert.Len(t, decs, 3) -} diff --git a/server/service/endpoint_import_config.go b/server/service/endpoint_import_config.go deleted file mode 100644 index a15b8df85..000000000 --- a/server/service/endpoint_import_config.go +++ /dev/null @@ -1,43 +0,0 @@ -package service - -import ( - "context" - - "github.com/go-kit/kit/endpoint" - "github.com/kolide/fleet/server/kolide" -) - -type importRequest struct { - DryRun bool `json:"dry_run"` - // Config contains a JSON osquery config supplied by the end user - Config string `json:"config"` - // ExternalPackConfigs contains a map of external Pack configs keyed by - // Pack name, this includes external packs referenced by the globbing - // feature. Not in the case of globbed packs, we expect the user to - // generate unique pack names since we don't know what they are, these - // names must be included in the GlobPackNames field so that we can - // validate that they've been accounted for. - ExternalPackConfigs map[string]string `json:"external_pack_configs"` - // GlobPackNames list of user generated names for external packs - // referenced by the glob feature, the JSON for the globbed packs - // is stored in ExternalPackConfigs keyed by the GlobPackName - GlobPackNames []string `json:"glob_pack_names"` -} - -type importResponse struct { - Response *kolide.ImportConfigResponse `json:"response,omitempty"` - Err error `json:"error,omitempty"` -} - -func (ir importResponse) error() error { return ir.Err } - -func makeImportConfigEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - config := request.(kolide.ImportConfig) - resp, err := svc.ImportConfig(ctx, &config) - if err != nil { - return importResponse{Err: err}, nil - } - return importResponse{Response: resp}, nil - } -} diff --git a/server/service/endpoint_import_config_test.go b/server/service/endpoint_import_config_test.go deleted file mode 100644 index 8c7c1ba1a..000000000 --- a/server/service/endpoint_import_config_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/kolide/fleet/server/kolide" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func testImportConfigWithGlob(t *testing.T, r *testResource) { - testJSON := ` -{ - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":10},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"*\":\"/path/to/glob/*\",\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":1200,\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3600\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"glob\":[\"globpack\"],\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}", - "external_pack_configs": { - "external_pack": "{\"discovery\":[\"select pid from processes where name = 'baz';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from something;\",\"interval\":1200,\"description\":\"Check something.\"}}}", - "globpack": "{\"discovery\":[\"select pid from processes where name = 'zip';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from other;\",\"interval\":1200,\"description\":\"Check other.\"}}}" - }, - "glob_pack_names": ["globpack"] -} -` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - var impResponse importResponse - err = json.NewDecoder(resp.Body).Decode(&impResponse) - require.Nil(t, err) - assert.Equal(t, 4, impResponse.Response.ImportStatusBySection[kolide.PacksSection].ImportCount) -} - -func testImportConfigWithInvalidPlatform(t *testing.T, r *testResource) { - testJSON := ` -{ - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":10},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"*\":\"/path/to/glob/*\",\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"foo\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":1200,\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3600\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"glob\":[\"globpack\"],\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}", - "external_pack_configs": { - "external_pack": "{\"discovery\":[\"select pid from processes where name = 'baz';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from something;\",\"interval\":1200,\"description\":\"Check something.\"}}}", - "globpack": "{\"discovery\":[\"select pid from processes where name = 'zip';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from other;\",\"interval\":1200,\"description\":\"Check other.\"}}}" - }, - "glob_pack_names": ["globpack"] -} -` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - var v mockValidationError - err = json.NewDecoder(resp.Body).Decode(&v) - require.Nil(t, err) - require.Len(t, v.Errors, 1) - assert.Equal(t, "'foo' is not a valid platform", v.Errors[0].Reason) -} - -func testImportConfigWithMissingGlob(t *testing.T, r *testResource) { - testJSON := ` - { - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":10},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"*\":\"/path/to/glob/*\",\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":1200,\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3600\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}", - "external_pack_configs": { - "external_pack": "{\"discovery\":[\"select pid from processes where name = 'baz';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from something;\",\"interval\":1200,\"description\":\"Check something.\"}}}" - } - } - ` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - var v mockValidationError - err = json.NewDecoder(resp.Body).Decode(&v) - require.Nil(t, err) - require.Len(t, v.Errors, 1) - assert.Equal(t, "missing glob packs", v.Errors[0].Reason) - -} - -func testImportConfigWithIntAsString(t *testing.T, r *testResource) { - - testJSON := ` - { - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":\"10\"},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":\"1200\",\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3600\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}", - "external_pack_configs": { - "external_pack": "{\"discovery\":[\"select pid from processes where name = 'baz';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from something;\",\"interval\":1200,\"description\":\"Check something.\"}}}" - } - } - ` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - var impResponse importResponse - err = json.NewDecoder(resp.Body).Decode(&impResponse) - require.Nil(t, err) - assert.Equal(t, 2, impResponse.Response.ImportStatusBySection[kolide.YARASigSection].ImportCount) - assert.Equal(t, 4, impResponse.Response.ImportStatusBySection[kolide.DecoratorsSection].ImportCount) -} - -func testImportConfig(t *testing.T, r *testResource) { - - testJSON := ` - { - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":10},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":1200,\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3600\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}", - "external_pack_configs": { - "external_pack": "{\"discovery\":[\"select pid from processes where name = 'baz';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"something\":{\"query\":\"select * from something;\",\"interval\":1200,\"description\":\"Check something.\"}}}" - } - } - ` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - var impResponse importResponse - err = json.NewDecoder(resp.Body).Decode(&impResponse) - require.Nil(t, err) - assert.Equal(t, 2, impResponse.Response.ImportStatusBySection[kolide.YARASigSection].ImportCount) - assert.Equal(t, 4, impResponse.Response.ImportStatusBySection[kolide.DecoratorsSection].ImportCount) -} - -func testImportConfigMissingExternal(t *testing.T, r *testResource) { - testJSON := ` - { - "config": "{\"options\":{\"host_identifier\":\"hostname\",\"schedule_splay_percent\":10},\"schedule\":{\"macosx_kextstat\":{\"query\":\"SELECT * FROM kernel_extensions;\",\"interval\":10},\"foobar\":{\"query\":\"SELECT foo, bar, pid FROM foobar_table;\",\"interval\":600}},\"packs\":{\"external_pack\":\"/path/to/external_pack.conf\",\"internal_pack\":{\"discovery\":[\"select pid from processes where name = 'foobar';\",\"select count(*) from users where username like 'www%';\"],\"platform\":\"linux\",\"version\":\"1.5.2\",\"queries\":{\"active_directory\":{\"query\":\"select * from ad_config;\",\"interval\":1200,\"description\":\"Check each user's active directory cached settings.\"}}}},\"decorators\":{\"load\":[\"SELECT version FROM osquery_info\",\"SELECT uuid AS host_uuid FROM system_info\"],\"always\":[\"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;\"],\"interval\":{\"3603\":[\"SELECT total_seconds AS uptime FROM uptime;\"]}},\"yara\":{\"signatures\":{\"sig_group_1\":[\"/Users/wxs/sigs/foo.sig\",\"/Users/wxs/sigs/bar.sig\"],\"sig_group_2\":[\"/Users/wxs/sigs/baz.sig\"]},\"file_paths\":{\"system_binaries\":[\"sig_group_1\"],\"tmp\":[\"sig_group_1\",\"sig_group_2\"]}},\"file_paths\":{\"system_binaries\":[\"/usr/bin/%\",\"/usr/sbin/%\"],\"tmp\":[\"/Users/%/tmp/%%\",\"/tmp/%\"]}}" - } - ` - buff := bytes.NewBufferString(testJSON) - req, err := http.NewRequest("POST", r.server.URL+"/api/v1/kolide/osquery/config/import", buff) - require.Nil(t, err) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.adminToken)) - client := &http.Client{} - resp, err := client.Do(req) - require.Nil(t, err) - - var v mockValidationError - err = json.NewDecoder(resp.Body).Decode(&v) - require.Nil(t, err) - require.Len(t, v.Errors, 2) - assert.Equal(t, "missing content for 'external_pack'", v.Errors[0].Reason) - assert.Equal(t, "interval '3603' must be divisible by 60", v.Errors[1].Reason) - -} diff --git a/server/service/endpoint_labels.go b/server/service/endpoint_labels.go index 5266630b0..2e7ce6a8d 100644 --- a/server/service/endpoint_labels.go +++ b/server/service/endpoint_labels.go @@ -102,45 +102,12 @@ func makeListLabelsEndpoint(svc kolide.Service) endpoint.Endpoint { } } -//////////////////////////////////////////////////////////////////////////////// -// Create Label -//////////////////////////////////////////////////////////////////////////////// - -type createLabelRequest struct { - payload kolide.LabelPayload -} - -type createLabelResponse struct { - Label labelResponse `json:"label"` - Err error `json:"error,omitempty"` -} - -func (r createLabelResponse) error() error { return r.Err } - -func makeCreateLabelEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(createLabelRequest) - - label, err := svc.NewLabel(ctx, req.payload) - if err != nil { - return createLabelResponse{Err: err}, nil - } - - labelResp, err := labelResponseForLabel(ctx, svc, label) - if err != nil { - return createLabelResponse{Err: err}, nil - } - - return createLabelResponse{Label: *labelResp}, nil - } -} - //////////////////////////////////////////////////////////////////////////////// // Delete Label //////////////////////////////////////////////////////////////////////////////// type deleteLabelRequest struct { - ID uint + Name string } type deleteLabelResponse struct { @@ -152,7 +119,7 @@ func (r deleteLabelResponse) error() error { return r.Err } func makeDeleteLabelEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(deleteLabelRequest) - err := svc.DeleteLabel(ctx, req.ID) + err := svc.DeleteLabel(ctx, req.Name) if err != nil { return deleteLabelResponse{Err: err}, nil } @@ -161,34 +128,69 @@ func makeDeleteLabelEndpoint(svc kolide.Service) endpoint.Endpoint { } //////////////////////////////////////////////////////////////////////////////// -// Modify Label +// Apply Label Specs //////////////////////////////////////////////////////////////////////////////// -type modifyLabelRequest struct { - ID uint - payload kolide.ModifyLabelPayload +type applyLabelSpecsRequest struct { + Specs []*kolide.LabelSpec `json:"specs"` } -type modifyLabelResponse struct { - Label labelResponse `json:"label"` - Err error `json:"error,omitempty"` +type applyLabelSpecsResponse struct { + Err error `json:"error,omitempty"` } -func (r modifyLabelResponse) error() error { return r.Err } +func (r applyLabelSpecsResponse) error() error { return r.Err } -func makeModifyLabelEndpoint(svc kolide.Service) endpoint.Endpoint { +func makeApplyLabelSpecsEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(modifyLabelRequest) - label, err := svc.ModifyLabel(ctx, req.ID, req.payload) + req := request.(applyLabelSpecsRequest) + err := svc.ApplyLabelSpecs(ctx, req.Specs) if err != nil { - return modifyLabelResponse{Err: err}, nil + return applyLabelSpecsResponse{Err: err}, nil } - - labelResp, err := labelResponseForLabel(ctx, svc, label) - if err != nil { - return modifyLabelResponse{Err: err}, nil - } - - return modifyLabelResponse{Label: *labelResp}, err + return applyLabelSpecsResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Label Specs +//////////////////////////////////////////////////////////////////////////////// + +type getLabelSpecsResponse struct { + Specs []*kolide.LabelSpec `json:"specs"` + Err error `json:"error,omitempty"` +} + +func (r getLabelSpecsResponse) error() error { return r.Err } + +func makeGetLabelSpecsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + specs, err := svc.GetLabelSpecs(ctx) + if err != nil { + return getLabelSpecsResponse{Err: err}, nil + } + return getLabelSpecsResponse{Specs: specs}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Label Spec +//////////////////////////////////////////////////////////////////////////////// + +type getLabelSpecResponse struct { + Spec *kolide.LabelSpec `json:"specs,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r getLabelSpecResponse) error() error { return r.Err } + +func makeGetLabelSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getGenericSpecRequest) + spec, err := svc.GetLabelSpec(ctx, req.Name) + if err != nil { + return getLabelSpecResponse{Err: err}, nil + } + return getLabelSpecResponse{Spec: spec}, nil } } diff --git a/server/service/endpoint_middleware.go b/server/service/endpoint_middleware.go index 2907264fa..01fb9d80b 100644 --- a/server/service/endpoint_middleware.go +++ b/server/service/endpoint_middleware.go @@ -2,8 +2,6 @@ package service import ( "context" - "errors" - "fmt" "reflect" jwt "github.com/dgrijalva/jwt-go" @@ -12,6 +10,7 @@ import ( "github.com/kolide/fleet/server/contexts/token" "github.com/kolide/fleet/server/contexts/viewer" "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" ) var errNoContext = errors.New("context key not set") @@ -88,13 +87,16 @@ func authenticatedUser(jwtKey string, svc kolide.Service, next endpoint.Endpoint func authViewer(ctx context.Context, jwtKey string, bearerToken token.Token, svc kolide.Service) (*viewer.Viewer, error) { jwtToken, err := jwt.Parse(string(bearerToken), func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + return nil, errors.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte(jwtKey), nil }) if err != nil { return nil, authError{reason: err.Error()} } + if jwtToken.Valid != true { + return nil, authError{reason: "invalid jwt token"} + } claims, ok := jwtToken.Claims.(jwt.MapClaims) if !ok { return nil, authError{reason: "no jwt claims"} diff --git a/server/service/endpoint_osquery.go b/server/service/endpoint_osquery.go index ca0320b8f..d8cc9ddbf 100644 --- a/server/service/endpoint_osquery.go +++ b/server/service/endpoint_osquery.go @@ -44,8 +44,8 @@ type getClientConfigRequest struct { } type getClientConfigResponse struct { - kolide.OsqueryConfig - Err error `json:"error,omitempty"` + Config map[string]interface{} + Err error `json:"error,omitempty"` } func (r getClientConfigResponse) error() error { return r.Err } @@ -56,7 +56,7 @@ func makeGetClientConfigEndpoint(svc kolide.Service) endpoint.Endpoint { if err != nil { return getClientConfigResponse{Err: err}, nil } - return getClientConfigResponse{OsqueryConfig: *config}, nil + return getClientConfigResponse{Config: config}, nil } } diff --git a/server/service/endpoint_osquery_options.go b/server/service/endpoint_osquery_options.go new file mode 100644 index 000000000..cd5ffd399 --- /dev/null +++ b/server/service/endpoint_osquery_options.go @@ -0,0 +1,54 @@ +package service + +import ( + "context" + + "github.com/go-kit/kit/endpoint" + "github.com/kolide/fleet/server/kolide" +) + +//////////////////////////////////////////////////////////////////////////////// +// Apply Options Spec +//////////////////////////////////////////////////////////////////////////////// + +type applyOsqueryOptionsSpecRequest struct { + Spec *kolide.OptionsSpec `json:"spec"` +} + +type applyOsqueryOptionsSpecResponse struct { + Err error `json:"error,omitempty"` +} + +func (r applyOsqueryOptionsSpecResponse) error() error { return r.Err } + +func makeApplyOsqueryOptionsSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(applyOsqueryOptionsSpecRequest) + err := svc.ApplyOptionsSpec(ctx, req.Spec) + if err != nil { + return applyOsqueryOptionsSpecResponse{Err: err}, nil + } + return applyOsqueryOptionsSpecResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Options Spec +//////////////////////////////////////////////////////////////////////////////// + +type getOsqueryOptionsSpecResponse struct { + Spec *kolide.OptionsSpec `json:"spec"` + Err error `json:"error,omitempty"` +} + +func (r getOsqueryOptionsSpecResponse) error() error { return r.Err } + +func makeGetOsqueryOptionsSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + spec, err := svc.GetOptionsSpec(ctx) + if err != nil { + return getOsqueryOptionsSpecResponse{Err: err}, nil + } + return getOsqueryOptionsSpecResponse{Spec: spec}, nil + } +} diff --git a/server/service/endpoint_packs.go b/server/service/endpoint_packs.go index 4e722239b..1b2876381 100644 --- a/server/service/endpoint_packs.go +++ b/server/service/endpoint_packs.go @@ -125,81 +125,12 @@ func makeListPacksEndpoint(svc kolide.Service) endpoint.Endpoint { } } -//////////////////////////////////////////////////////////////////////////////// -// Create Pack -//////////////////////////////////////////////////////////////////////////////// - -type createPackRequest struct { - payload kolide.PackPayload -} - -type createPackResponse struct { - Pack packResponse `json:"pack,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r createPackResponse) error() error { return r.Err } - -func makeCreatePackEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(createPackRequest) - pack, err := svc.NewPack(ctx, req.payload) - if err != nil { - return createPackResponse{Err: err}, nil - } - - resp, err := packResponseForPack(ctx, svc, *pack) - if err != nil { - return createPackResponse{Err: err}, nil - } - - return createPackResponse{ - Pack: *resp, - }, nil - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Modify Pack -//////////////////////////////////////////////////////////////////////////////// - -type modifyPackRequest struct { - ID uint - payload kolide.PackPayload -} - -type modifyPackResponse struct { - Pack packResponse `json:"pack,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r modifyPackResponse) error() error { return r.Err } - -func makeModifyPackEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(modifyPackRequest) - pack, err := svc.ModifyPack(ctx, req.ID, req.payload) - if err != nil { - return modifyPackResponse{Err: err}, nil - } - - resp, err := packResponseForPack(ctx, svc, *pack) - if err != nil { - return modifyPackResponse{Err: err}, nil - } - - return modifyPackResponse{ - Pack: *resp, - }, nil - } -} - //////////////////////////////////////////////////////////////////////////////// // Delete Pack //////////////////////////////////////////////////////////////////////////////// type deletePackRequest struct { - ID uint + Name string } type deletePackResponse struct { @@ -211,10 +142,78 @@ func (r deletePackResponse) error() error { return r.Err } func makeDeletePackEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(deletePackRequest) - err := svc.DeletePack(ctx, req.ID) + err := svc.DeletePack(ctx, req.Name) if err != nil { return deletePackResponse{Err: err}, nil } return deletePackResponse{}, nil } } + +//////////////////////////////////////////////////////////////////////////////// +// Apply Pack Specs +//////////////////////////////////////////////////////////////////////////////// + +type applyPackSpecsRequest struct { + Specs []*kolide.PackSpec `json:"specs"` +} + +type applyPackSpecsResponse struct { + Err error `json:"error,omitempty"` +} + +func (r applyPackSpecsResponse) error() error { return r.Err } + +func makeApplyPackSpecsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(applyPackSpecsRequest) + err := svc.ApplyPackSpecs(ctx, req.Specs) + if err != nil { + return applyPackSpecsResponse{Err: err}, nil + } + return applyPackSpecsResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Pack Specs +//////////////////////////////////////////////////////////////////////////////// + +type getPackSpecsResponse struct { + Specs []*kolide.PackSpec `json:"specs"` + Err error `json:"error,omitempty"` +} + +func (r getPackSpecsResponse) error() error { return r.Err } + +func makeGetPackSpecsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + specs, err := svc.GetPackSpecs(ctx) + if err != nil { + return getPackSpecsResponse{Err: err}, nil + } + return getPackSpecsResponse{Specs: specs}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Pack Spec +//////////////////////////////////////////////////////////////////////////////// + +type getPackSpecResponse struct { + Spec *kolide.PackSpec `json:"specs,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r getPackSpecResponse) error() error { return r.Err } + +func makeGetPackSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getGenericSpecRequest) + spec, err := svc.GetPackSpec(ctx, req.Name) + if err != nil { + return getPackSpecResponse{Err: err}, nil + } + return getPackSpecResponse{Spec: spec}, nil + } +} diff --git a/server/service/endpoint_queries.go b/server/service/endpoint_queries.go index fc101776d..6490839eb 100644 --- a/server/service/endpoint_queries.go +++ b/server/service/endpoint_queries.go @@ -121,7 +121,7 @@ func makeModifyQueryEndpoint(svc kolide.Service) endpoint.Endpoint { //////////////////////////////////////////////////////////////////////////////// type deleteQueryRequest struct { - ID uint + Name string } type deleteQueryResponse struct { @@ -133,7 +133,7 @@ func (r deleteQueryResponse) error() error { return r.Err } func makeDeleteQueryEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(deleteQueryRequest) - err := svc.DeleteQuery(ctx, req.ID) + err := svc.DeleteQuery(ctx, req.Name) if err != nil { return deleteQueryResponse{Err: err}, nil } @@ -166,3 +166,71 @@ func makeDeleteQueriesEndpoint(svc kolide.Service) endpoint.Endpoint { return deleteQueriesResponse{Deleted: deleted}, nil } } + +//////////////////////////////////////////////////////////////////////////////// +// Apply Query Specs +//////////////////////////////////////////////////////////////////////////////// + +type applyQuerySpecsRequest struct { + Specs []*kolide.QuerySpec `json:"specs"` +} + +type applyQuerySpecsResponse struct { + Err error `json:"error,omitempty"` +} + +func (r applyQuerySpecsResponse) error() error { return r.Err } + +func makeApplyQuerySpecsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(applyQuerySpecsRequest) + err := svc.ApplyQuerySpecs(ctx, req.Specs) + if err != nil { + return applyQuerySpecsResponse{Err: err}, nil + } + return applyQuerySpecsResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Query Specs +//////////////////////////////////////////////////////////////////////////////// + +type getQuerySpecsResponse struct { + Specs []*kolide.QuerySpec `json:"specs"` + Err error `json:"error,omitempty"` +} + +func (r getQuerySpecsResponse) error() error { return r.Err } + +func makeGetQuerySpecsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + specs, err := svc.GetQuerySpecs(ctx) + if err != nil { + return getQuerySpecsResponse{Err: err}, nil + } + return getQuerySpecsResponse{Specs: specs}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Query Spec +//////////////////////////////////////////////////////////////////////////////// + +type getQuerySpecResponse struct { + Spec *kolide.QuerySpec `json:"specs,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r getQuerySpecResponse) error() error { return r.Err } + +func makeGetQuerySpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getGenericSpecRequest) + spec, err := svc.GetQuerySpec(ctx, req.Name) + if err != nil { + return getQuerySpecResponse{Err: err}, nil + } + return getQuerySpecResponse{Spec: spec}, nil + } +} diff --git a/server/service/endpoint_scheduled_queries.go b/server/service/endpoint_scheduled_queries.go index 391c6fd8f..a5b914ebd 100644 --- a/server/service/endpoint_scheduled_queries.go +++ b/server/service/endpoint_scheduled_queries.go @@ -7,42 +7,6 @@ import ( "github.com/kolide/fleet/server/kolide" ) -//////////////////////////////////////////////////////////////////////////////// -// Get Scheduled Query -//////////////////////////////////////////////////////////////////////////////// - -type getScheduledQueryRequest struct { - ID uint -} - -type scheduledQueryResponse struct { - kolide.ScheduledQuery -} - -type getScheduledQueryResponse struct { - Scheduled scheduledQueryResponse `json:"scheduled,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r getScheduledQueryResponse) error() error { return r.Err } - -func makeGetScheduledQueryEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(getScheduledQueryRequest) - - sq, err := svc.GetScheduledQuery(ctx, req.ID) - if err != nil { - return getScheduledQueryResponse{Err: err}, nil - } - - return getScheduledQueryResponse{ - Scheduled: scheduledQueryResponse{ - ScheduledQuery: *sq, - }, - }, nil - } -} - //////////////////////////////////////////////////////////////////////////////// // Get Scheduled Queries In Pack //////////////////////////////////////////////////////////////////////////////// @@ -52,6 +16,10 @@ type getScheduledQueriesInPackRequest struct { ListOptions kolide.ListOptions } +type scheduledQueryResponse struct { + kolide.ScheduledQuery +} + type getScheduledQueriesInPackResponse struct { Scheduled []scheduledQueryResponse `json:"scheduled"` Err error `json:"error,omitempty"` @@ -78,106 +46,3 @@ func makeGetScheduledQueriesInPackEndpoint(svc kolide.Service) endpoint.Endpoint return resp, nil } } - -//////////////////////////////////////////////////////////////////////////////// -// Schedule Query -//////////////////////////////////////////////////////////////////////////////// - -type scheduleQueryRequest struct { - PackID uint `json:"pack_id"` - QueryID uint `json:"query_id"` - Interval uint `json:"interval"` - Snapshot *bool `json:"snapshot"` - Removed *bool `json:"removed"` - Platform *string `json:"platform"` - Version *string `json:"version"` - Shard *uint `json:"shard"` -} - -type scheduleQueryResponse struct { - Scheduled scheduledQueryResponse `json:"scheduled"` - Err error `json:"error,omitempty"` -} - -func makeScheduleQueryEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(scheduleQueryRequest) - - scheduled, err := svc.ScheduleQuery(ctx, &kolide.ScheduledQuery{ - PackID: req.PackID, - QueryID: req.QueryID, - Interval: req.Interval, - Snapshot: req.Snapshot, - Removed: req.Removed, - Platform: req.Platform, - Version: req.Version, - Shard: req.Shard, - }) - if err != nil { - return scheduleQueryResponse{Err: err}, nil - } - return scheduleQueryResponse{Scheduled: scheduledQueryResponse{ - ScheduledQuery: *scheduled, - }}, nil - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Modify Scheduled Query -//////////////////////////////////////////////////////////////////////////////// - -type modifyScheduledQueryRequest struct { - ID uint - payload kolide.ScheduledQueryPayload -} - -type modifyScheduledQueryResponse struct { - Scheduled scheduledQueryResponse `json:"scheduled,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r modifyScheduledQueryResponse) error() error { return r.Err } - -func makeModifyScheduledQueryEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(modifyScheduledQueryRequest) - - sq, err := svc.ModifyScheduledQuery(ctx, req.ID, req.payload) - if err != nil { - return modifyScheduledQueryResponse{Err: err}, nil - } - - return modifyScheduledQueryResponse{ - Scheduled: scheduledQueryResponse{ - ScheduledQuery: *sq, - }, - }, nil - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Delete Scheduled Query -//////////////////////////////////////////////////////////////////////////////// - -type deleteScheduledQueryRequest struct { - ID uint -} - -type deleteScheduledQueryResponse struct { - Err error `json:"error,omitempty"` -} - -func (r deleteScheduledQueryResponse) error() error { return r.Err } - -func makeDeleteScheduledQueryEndpoint(svc kolide.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(deleteScheduledQueryRequest) - - err := svc.DeleteScheduledQuery(ctx, req.ID) - if err != nil { - return deleteScheduledQueryResponse{Err: err}, nil - } - - return deleteScheduledQueryResponse{}, nil - } -} diff --git a/server/service/endpoint_setup.go b/server/service/endpoint_setup.go index c2e625bfc..58104f34a 100644 --- a/server/service/endpoint_setup.go +++ b/server/service/endpoint_setup.go @@ -5,13 +5,14 @@ import ( "github.com/go-kit/kit/endpoint" "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" ) type setupRequest struct { Admin *kolide.UserPayload `json:"admin"` OrgInfo *kolide.OrgInfo `json:"org_info"` - KolideServerURL *string `json:"kolide_server_url"` - EnrollSecret *string `json:"osquery_enroll_secret"` + KolideServerURL *string `json:"kolide_server_url,omitempty"` + EnrollSecret *string `json:"osquery_enroll_secret,omitempty"` } type setupResponse struct { @@ -51,6 +52,14 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint { // creating the user should be the last action. If there's a user // present and other errors occur, the setup endpoint closes. if req.Admin != nil { + if *req.Admin.Email == "" { + err := errors.Errorf("admin email cannot be empty") + return setupResponse{Err: err}, nil + } + if *req.Admin.Password == "" { + err := errors.Errorf("admin password cannot be empty") + return setupResponse{Err: err}, nil + } admin, err = svc.NewAdminCreatedUser(ctx, *req.Admin) if err != nil { return setupResponse{Err: err}, nil diff --git a/server/service/endpoint_test.go b/server/service/endpoint_test.go index 2c04957c1..7784c9756 100644 --- a/server/service/endpoint_test.go +++ b/server/service/endpoint_test.go @@ -107,23 +107,10 @@ var testFunctions = [...]func(*testing.T, *testResource){ testModifyOptions, testModifyOptionsValidationFail, testOptionNotFound, - testImportConfig, - testImportConfigMissingExternal, - testImportConfigWithMissingGlob, - testImportConfigWithGlob, - testImportConfigWithIntAsString, testAdminUserSetAdmin, testNonAdminUserSetAdmin, testAdminUserSetEnabled, testNonAdminUserSetEnabled, - testModifyDecorator, - testListDecorator, - testNewDecorator, - testNewDecoratorFailType, - testNewDecoratorFailValidation, - testDeleteDecorator, - testModifyDecoratorNoChanges, - testImportConfigWithInvalidPlatform, } func TestEndpoints(t *testing.T) { diff --git a/server/service/handler.go b/server/service/handler.go index 02c5f7867..202b41716 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -15,77 +15,76 @@ import ( // KolideEndpoints is a collection of RPC endpoints implemented by the Kolide API. type KolideEndpoints struct { - Login endpoint.Endpoint - Logout endpoint.Endpoint - ForgotPassword endpoint.Endpoint - ResetPassword endpoint.Endpoint - Me endpoint.Endpoint - ChangePassword endpoint.Endpoint - CreateUser endpoint.Endpoint - GetUser endpoint.Endpoint - ListUsers endpoint.Endpoint - ModifyUser endpoint.Endpoint - AdminUser endpoint.Endpoint - EnableUser endpoint.Endpoint - RequirePasswordReset endpoint.Endpoint - PerformRequiredPasswordReset endpoint.Endpoint - GetSessionsForUserInfo endpoint.Endpoint - DeleteSessionsForUser endpoint.Endpoint - GetSessionInfo endpoint.Endpoint - DeleteSession endpoint.Endpoint - GetAppConfig endpoint.Endpoint - ModifyAppConfig endpoint.Endpoint - CreateInvite endpoint.Endpoint - ListInvites endpoint.Endpoint - DeleteInvite endpoint.Endpoint - VerifyInvite endpoint.Endpoint - GetQuery endpoint.Endpoint - ListQueries endpoint.Endpoint - CreateQuery endpoint.Endpoint - ModifyQuery endpoint.Endpoint - DeleteQuery endpoint.Endpoint - DeleteQueries endpoint.Endpoint - CreateDistributedQueryCampaign endpoint.Endpoint - GetPack endpoint.Endpoint - ListPacks endpoint.Endpoint - CreatePack endpoint.Endpoint - ModifyPack endpoint.Endpoint - DeletePack endpoint.Endpoint - ScheduleQuery endpoint.Endpoint - GetScheduledQueriesInPack endpoint.Endpoint - GetScheduledQuery endpoint.Endpoint - ModifyScheduledQuery endpoint.Endpoint - DeleteScheduledQuery endpoint.Endpoint - EnrollAgent endpoint.Endpoint - GetClientConfig endpoint.Endpoint - GetDistributedQueries endpoint.Endpoint - SubmitDistributedQueryResults endpoint.Endpoint - SubmitLogs endpoint.Endpoint - GetLabel endpoint.Endpoint - ListLabels endpoint.Endpoint - CreateLabel endpoint.Endpoint - DeleteLabel endpoint.Endpoint - ModifyLabel endpoint.Endpoint - ListDecorators endpoint.Endpoint - NewDecorator endpoint.Endpoint - ModifyDecorator endpoint.Endpoint - DeleteDecorator endpoint.Endpoint - GetHost endpoint.Endpoint - DeleteHost endpoint.Endpoint - ListHosts endpoint.Endpoint - GetHostSummary endpoint.Endpoint - SearchTargets endpoint.Endpoint - GetOptions endpoint.Endpoint - ModifyOptions endpoint.Endpoint - ResetOptions endpoint.Endpoint - ImportConfig endpoint.Endpoint - GetCertificate endpoint.Endpoint - ChangeEmail endpoint.Endpoint - InitiateSSO endpoint.Endpoint - CallbackSSO endpoint.Endpoint - SSOSettings endpoint.Endpoint - GetFIM endpoint.Endpoint - ModifyFIM endpoint.Endpoint + Login endpoint.Endpoint + Logout endpoint.Endpoint + ForgotPassword endpoint.Endpoint + ResetPassword endpoint.Endpoint + Me endpoint.Endpoint + ChangePassword endpoint.Endpoint + CreateUser endpoint.Endpoint + GetUser endpoint.Endpoint + ListUsers endpoint.Endpoint + ModifyUser endpoint.Endpoint + AdminUser endpoint.Endpoint + EnableUser endpoint.Endpoint + RequirePasswordReset endpoint.Endpoint + PerformRequiredPasswordReset endpoint.Endpoint + GetSessionsForUserInfo endpoint.Endpoint + DeleteSessionsForUser endpoint.Endpoint + GetSessionInfo endpoint.Endpoint + DeleteSession endpoint.Endpoint + GetAppConfig endpoint.Endpoint + ModifyAppConfig endpoint.Endpoint + CreateInvite endpoint.Endpoint + ListInvites endpoint.Endpoint + DeleteInvite endpoint.Endpoint + VerifyInvite endpoint.Endpoint + GetQuery endpoint.Endpoint + ListQueries endpoint.Endpoint + CreateQuery endpoint.Endpoint + ModifyQuery endpoint.Endpoint + DeleteQuery endpoint.Endpoint + DeleteQueries endpoint.Endpoint + ApplyQuerySpecs endpoint.Endpoint + GetQuerySpecs endpoint.Endpoint + GetQuerySpec endpoint.Endpoint + CreateDistributedQueryCampaign endpoint.Endpoint + CreateDistributedQueryCampaignByNames endpoint.Endpoint + GetPack endpoint.Endpoint + ListPacks endpoint.Endpoint + DeletePack endpoint.Endpoint + GetScheduledQueriesInPack endpoint.Endpoint + ApplyPackSpecs endpoint.Endpoint + GetPackSpecs endpoint.Endpoint + GetPackSpec endpoint.Endpoint + EnrollAgent endpoint.Endpoint + GetClientConfig endpoint.Endpoint + GetDistributedQueries endpoint.Endpoint + SubmitDistributedQueryResults endpoint.Endpoint + SubmitLogs endpoint.Endpoint + GetLabel endpoint.Endpoint + ListLabels endpoint.Endpoint + DeleteLabel endpoint.Endpoint + ApplyLabelSpecs endpoint.Endpoint + GetLabelSpecs endpoint.Endpoint + GetLabelSpec endpoint.Endpoint + GetHost endpoint.Endpoint + DeleteHost endpoint.Endpoint + ListHosts endpoint.Endpoint + GetHostSummary endpoint.Endpoint + SearchTargets endpoint.Endpoint + GetOptions endpoint.Endpoint + ModifyOptions endpoint.Endpoint + ResetOptions endpoint.Endpoint + ApplyOsqueryOptionsSpec endpoint.Endpoint + GetOsqueryOptionsSpec endpoint.Endpoint + GetCertificate endpoint.Endpoint + ChangeEmail endpoint.Endpoint + InitiateSSO endpoint.Endpoint + CallbackSSO endpoint.Endpoint + SSOSettings endpoint.Endpoint + GetFIM endpoint.Endpoint + ModifyFIM endpoint.Endpoint } // MakeKolideServerEndpoints creates the Kolide API endpoints. @@ -118,51 +117,50 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint RequirePasswordReset: authenticatedUser(jwtKey, svc, mustBeAdmin(makeRequirePasswordResetEndpoint(svc))), // PerformRequiredPasswordReset needs only to authenticate the // logged in user - PerformRequiredPasswordReset: authenticatedUser(jwtKey, svc, makePerformRequiredPasswordResetEndpoint(svc)), - GetSessionsForUserInfo: authenticatedUser(jwtKey, svc, canReadUser(makeGetInfoAboutSessionsForUserEndpoint(svc))), - DeleteSessionsForUser: authenticatedUser(jwtKey, svc, canModifyUser(makeDeleteSessionsForUserEndpoint(svc))), - GetSessionInfo: authenticatedUser(jwtKey, svc, mustBeAdmin(makeGetInfoAboutSessionEndpoint(svc))), - DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))), - GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))), - ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigEndpoint(svc))), - CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))), - ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), - DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), - GetQuery: authenticatedUser(jwtKey, svc, makeGetQueryEndpoint(svc)), - ListQueries: authenticatedUser(jwtKey, svc, makeListQueriesEndpoint(svc)), - CreateQuery: authenticatedUser(jwtKey, svc, makeCreateQueryEndpoint(svc)), - ModifyQuery: authenticatedUser(jwtKey, svc, makeModifyQueryEndpoint(svc)), - DeleteQuery: authenticatedUser(jwtKey, svc, makeDeleteQueryEndpoint(svc)), - DeleteQueries: authenticatedUser(jwtKey, svc, makeDeleteQueriesEndpoint(svc)), - CreateDistributedQueryCampaign: authenticatedUser(jwtKey, svc, makeCreateDistributedQueryCampaignEndpoint(svc)), + PerformRequiredPasswordReset: authenticatedUser(jwtKey, svc, makePerformRequiredPasswordResetEndpoint(svc)), + GetSessionsForUserInfo: authenticatedUser(jwtKey, svc, canReadUser(makeGetInfoAboutSessionsForUserEndpoint(svc))), + DeleteSessionsForUser: authenticatedUser(jwtKey, svc, canModifyUser(makeDeleteSessionsForUserEndpoint(svc))), + GetSessionInfo: authenticatedUser(jwtKey, svc, mustBeAdmin(makeGetInfoAboutSessionEndpoint(svc))), + DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))), + GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))), + ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigEndpoint(svc))), + CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))), + ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), + DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), + GetQuery: authenticatedUser(jwtKey, svc, makeGetQueryEndpoint(svc)), + ListQueries: authenticatedUser(jwtKey, svc, makeListQueriesEndpoint(svc)), + CreateQuery: authenticatedUser(jwtKey, svc, makeCreateQueryEndpoint(svc)), + ModifyQuery: authenticatedUser(jwtKey, svc, makeModifyQueryEndpoint(svc)), + DeleteQuery: authenticatedUser(jwtKey, svc, makeDeleteQueryEndpoint(svc)), + DeleteQueries: authenticatedUser(jwtKey, svc, makeDeleteQueriesEndpoint(svc)), + ApplyQuerySpecs: authenticatedUser(jwtKey, svc, makeApplyQuerySpecsEndpoint(svc)), + GetQuerySpecs: authenticatedUser(jwtKey, svc, makeGetQuerySpecsEndpoint(svc)), + GetQuerySpec: authenticatedUser(jwtKey, svc, makeGetQuerySpecEndpoint(svc)), + CreateDistributedQueryCampaign: authenticatedUser(jwtKey, svc, makeCreateDistributedQueryCampaignEndpoint(svc)), + CreateDistributedQueryCampaignByNames: authenticatedUser(jwtKey, svc, makeCreateDistributedQueryCampaignByNamesEndpoint(svc)), GetPack: authenticatedUser(jwtKey, svc, makeGetPackEndpoint(svc)), ListPacks: authenticatedUser(jwtKey, svc, makeListPacksEndpoint(svc)), - CreatePack: authenticatedUser(jwtKey, svc, makeCreatePackEndpoint(svc)), - ModifyPack: authenticatedUser(jwtKey, svc, makeModifyPackEndpoint(svc)), DeletePack: authenticatedUser(jwtKey, svc, makeDeletePackEndpoint(svc)), - ScheduleQuery: authenticatedUser(jwtKey, svc, makeScheduleQueryEndpoint(svc)), GetScheduledQueriesInPack: authenticatedUser(jwtKey, svc, makeGetScheduledQueriesInPackEndpoint(svc)), - GetScheduledQuery: authenticatedUser(jwtKey, svc, makeGetScheduledQueryEndpoint(svc)), - ModifyScheduledQuery: authenticatedUser(jwtKey, svc, makeModifyScheduledQueryEndpoint(svc)), - DeleteScheduledQuery: authenticatedUser(jwtKey, svc, makeDeleteScheduledQueryEndpoint(svc)), + ApplyPackSpecs: authenticatedUser(jwtKey, svc, makeApplyPackSpecsEndpoint(svc)), + GetPackSpecs: authenticatedUser(jwtKey, svc, makeGetPackSpecsEndpoint(svc)), + GetPackSpec: authenticatedUser(jwtKey, svc, makeGetPackSpecEndpoint(svc)), GetHost: authenticatedUser(jwtKey, svc, makeGetHostEndpoint(svc)), ListHosts: authenticatedUser(jwtKey, svc, makeListHostsEndpoint(svc)), GetHostSummary: authenticatedUser(jwtKey, svc, makeGetHostSummaryEndpoint(svc)), DeleteHost: authenticatedUser(jwtKey, svc, makeDeleteHostEndpoint(svc)), GetLabel: authenticatedUser(jwtKey, svc, makeGetLabelEndpoint(svc)), ListLabels: authenticatedUser(jwtKey, svc, makeListLabelsEndpoint(svc)), - CreateLabel: authenticatedUser(jwtKey, svc, makeCreateLabelEndpoint(svc)), DeleteLabel: authenticatedUser(jwtKey, svc, makeDeleteLabelEndpoint(svc)), - ModifyLabel: authenticatedUser(jwtKey, svc, makeModifyLabelEndpoint(svc)), - ListDecorators: authenticatedUser(jwtKey, svc, makeListDecoratorsEndpoint(svc)), - NewDecorator: authenticatedUser(jwtKey, svc, makeNewDecoratorEndpoint(svc)), - ModifyDecorator: authenticatedUser(jwtKey, svc, makeModifyDecoratorEndpoint(svc)), - DeleteDecorator: authenticatedUser(jwtKey, svc, makeDeleteDecoratorEndpoint(svc)), + ApplyLabelSpecs: authenticatedUser(jwtKey, svc, makeApplyLabelSpecsEndpoint(svc)), + GetLabelSpecs: authenticatedUser(jwtKey, svc, makeGetLabelSpecsEndpoint(svc)), + GetLabelSpec: authenticatedUser(jwtKey, svc, makeGetLabelSpecEndpoint(svc)), SearchTargets: authenticatedUser(jwtKey, svc, makeSearchTargetsEndpoint(svc)), GetOptions: authenticatedUser(jwtKey, svc, mustBeAdmin(makeGetOptionsEndpoint(svc))), ModifyOptions: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyOptionsEndpoint(svc))), ResetOptions: authenticatedUser(jwtKey, svc, mustBeAdmin(makeResetOptionsEndpoint(svc))), - ImportConfig: authenticatedUser(jwtKey, svc, makeImportConfigEndpoint(svc)), + ApplyOsqueryOptionsSpec: authenticatedUser(jwtKey, svc, makeApplyOsqueryOptionsSpecEndpoint(svc)), + GetOsqueryOptionsSpec: authenticatedUser(jwtKey, svc, makeGetOsqueryOptionsSpecEndpoint(svc)), GetCertificate: authenticatedUser(jwtKey, svc, makeCertificateEndpoint(svc)), ChangeEmail: authenticatedUser(jwtKey, svc, makeChangeEmailEndpoint(svc)), GetFIM: authenticatedUser(jwtKey, svc, makeGetFIMEndpoint(svc)), @@ -178,77 +176,76 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint } type kolideHandlers struct { - Login http.Handler - Logout http.Handler - ForgotPassword http.Handler - ResetPassword http.Handler - Me http.Handler - ChangePassword http.Handler - CreateUser http.Handler - GetUser http.Handler - ListUsers http.Handler - ModifyUser http.Handler - AdminUser http.Handler - EnableUser http.Handler - RequirePasswordReset http.Handler - PerformRequiredPasswordReset http.Handler - GetSessionsForUserInfo http.Handler - DeleteSessionsForUser http.Handler - GetSessionInfo http.Handler - DeleteSession http.Handler - GetAppConfig http.Handler - ModifyAppConfig http.Handler - CreateInvite http.Handler - ListInvites http.Handler - DeleteInvite http.Handler - VerifyInvite http.Handler - GetQuery http.Handler - ListQueries http.Handler - CreateQuery http.Handler - ModifyQuery http.Handler - DeleteQuery http.Handler - DeleteQueries http.Handler - CreateDistributedQueryCampaign http.Handler - GetPack http.Handler - ListPacks http.Handler - CreatePack http.Handler - ModifyPack http.Handler - DeletePack http.Handler - ScheduleQuery http.Handler - GetScheduledQueriesInPack http.Handler - GetScheduledQuery http.Handler - ModifyScheduledQuery http.Handler - DeleteScheduledQuery http.Handler - EnrollAgent http.Handler - GetClientConfig http.Handler - GetDistributedQueries http.Handler - SubmitDistributedQueryResults http.Handler - SubmitLogs http.Handler - GetLabel http.Handler - ListLabels http.Handler - CreateLabel http.Handler - DeleteLabel http.Handler - ModifyLabel http.Handler - ListDecorators http.Handler - NewDecorator http.Handler - ModifyDecorator http.Handler - DeleteDecorator http.Handler - GetHost http.Handler - DeleteHost http.Handler - ListHosts http.Handler - GetHostSummary http.Handler - SearchTargets http.Handler - GetOptions http.Handler - ModifyOptions http.Handler - ResetOptions http.Handler - ImportConfig http.Handler - GetCertificate http.Handler - ChangeEmail http.Handler - InitiateSSO http.Handler - CallbackSSO http.Handler - SettingsSSO http.Handler - ModifyFIM http.Handler - GetFIM http.Handler + Login http.Handler + Logout http.Handler + ForgotPassword http.Handler + ResetPassword http.Handler + Me http.Handler + ChangePassword http.Handler + CreateUser http.Handler + GetUser http.Handler + ListUsers http.Handler + ModifyUser http.Handler + AdminUser http.Handler + EnableUser http.Handler + RequirePasswordReset http.Handler + PerformRequiredPasswordReset http.Handler + GetSessionsForUserInfo http.Handler + DeleteSessionsForUser http.Handler + GetSessionInfo http.Handler + DeleteSession http.Handler + GetAppConfig http.Handler + ModifyAppConfig http.Handler + CreateInvite http.Handler + ListInvites http.Handler + DeleteInvite http.Handler + VerifyInvite http.Handler + GetQuery http.Handler + ListQueries http.Handler + CreateQuery http.Handler + ModifyQuery http.Handler + DeleteQuery http.Handler + DeleteQueries http.Handler + ApplyQuerySpecs http.Handler + GetQuerySpecs http.Handler + GetQuerySpec http.Handler + CreateDistributedQueryCampaign http.Handler + CreateDistributedQueryCampaignByNames http.Handler + GetPack http.Handler + ListPacks http.Handler + DeletePack http.Handler + GetScheduledQueriesInPack http.Handler + ApplyPackSpecs http.Handler + GetPackSpecs http.Handler + GetPackSpec http.Handler + EnrollAgent http.Handler + GetClientConfig http.Handler + GetDistributedQueries http.Handler + SubmitDistributedQueryResults http.Handler + SubmitLogs http.Handler + GetLabel http.Handler + ListLabels http.Handler + DeleteLabel http.Handler + ApplyLabelSpecs http.Handler + GetLabelSpecs http.Handler + GetLabelSpec http.Handler + GetHost http.Handler + DeleteHost http.Handler + ListHosts http.Handler + GetHostSummary http.Handler + SearchTargets http.Handler + GetOptions http.Handler + ModifyOptions http.Handler + ResetOptions http.Handler + ApplyOsqueryOptionsSpec http.Handler + GetOsqueryOptionsSpec http.Handler + GetCertificate http.Handler + ChangeEmail http.Handler + InitiateSSO http.Handler + CallbackSSO http.Handler + SettingsSSO http.Handler + ModifyFIM http.Handler + GetFIM http.Handler } func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *kolideHandlers { @@ -256,47 +253,48 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli return kithttp.NewServer(e, decodeFn, encodeResponse, opts...) } return &kolideHandlers{ - Login: newServer(e.Login, decodeLoginRequest), - Logout: newServer(e.Logout, decodeNoParamsRequest), - ForgotPassword: newServer(e.ForgotPassword, decodeForgotPasswordRequest), - ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), - Me: newServer(e.Me, decodeNoParamsRequest), - ChangePassword: newServer(e.ChangePassword, decodeChangePasswordRequest), - CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), - GetUser: newServer(e.GetUser, decodeGetUserRequest), - ListUsers: newServer(e.ListUsers, decodeListUsersRequest), - ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), - RequirePasswordReset: newServer(e.RequirePasswordReset, decodeRequirePasswordResetRequest), - PerformRequiredPasswordReset: newServer(e.PerformRequiredPasswordReset, decodePerformRequiredPasswordResetRequest), - EnableUser: newServer(e.EnableUser, decodeEnableUserRequest), - AdminUser: newServer(e.AdminUser, decodeAdminUserRequest), - GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), - DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), - GetSessionInfo: newServer(e.GetSessionInfo, decodeGetInfoAboutSessionRequest), - DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), - GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), - ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), - CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), - ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), - DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), - VerifyInvite: newServer(e.VerifyInvite, decodeVerifyInviteRequest), - GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), - ListQueries: newServer(e.ListQueries, decodeListQueriesRequest), - CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), - ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest), - DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest), - DeleteQueries: newServer(e.DeleteQueries, decodeDeleteQueriesRequest), - CreateDistributedQueryCampaign: newServer(e.CreateDistributedQueryCampaign, decodeCreateDistributedQueryCampaignRequest), + Login: newServer(e.Login, decodeLoginRequest), + Logout: newServer(e.Logout, decodeNoParamsRequest), + ForgotPassword: newServer(e.ForgotPassword, decodeForgotPasswordRequest), + ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), + Me: newServer(e.Me, decodeNoParamsRequest), + ChangePassword: newServer(e.ChangePassword, decodeChangePasswordRequest), + CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), + GetUser: newServer(e.GetUser, decodeGetUserRequest), + ListUsers: newServer(e.ListUsers, decodeListUsersRequest), + ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), + RequirePasswordReset: newServer(e.RequirePasswordReset, decodeRequirePasswordResetRequest), + PerformRequiredPasswordReset: newServer(e.PerformRequiredPasswordReset, decodePerformRequiredPasswordResetRequest), + EnableUser: newServer(e.EnableUser, decodeEnableUserRequest), + AdminUser: newServer(e.AdminUser, decodeAdminUserRequest), + GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), + DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), + GetSessionInfo: newServer(e.GetSessionInfo, decodeGetInfoAboutSessionRequest), + DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), + GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), + ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), + CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), + ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), + DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), + VerifyInvite: newServer(e.VerifyInvite, decodeVerifyInviteRequest), + GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), + ListQueries: newServer(e.ListQueries, decodeListQueriesRequest), + CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), + ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest), + DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest), + DeleteQueries: newServer(e.DeleteQueries, decodeDeleteQueriesRequest), + ApplyQuerySpecs: newServer(e.ApplyQuerySpecs, decodeApplyQuerySpecsRequest), + GetQuerySpecs: newServer(e.GetQuerySpecs, decodeNoParamsRequest), + GetQuerySpec: newServer(e.GetQuerySpec, decodeGetGenericSpecRequest), + CreateDistributedQueryCampaign: newServer(e.CreateDistributedQueryCampaign, decodeCreateDistributedQueryCampaignRequest), + CreateDistributedQueryCampaignByNames: newServer(e.CreateDistributedQueryCampaignByNames, decodeCreateDistributedQueryCampaignByNamesRequest), GetPack: newServer(e.GetPack, decodeGetPackRequest), ListPacks: newServer(e.ListPacks, decodeListPacksRequest), - CreatePack: newServer(e.CreatePack, decodeCreatePackRequest), - ModifyPack: newServer(e.ModifyPack, decodeModifyPackRequest), DeletePack: newServer(e.DeletePack, decodeDeletePackRequest), - ScheduleQuery: newServer(e.ScheduleQuery, decodeScheduleQueryRequest), GetScheduledQueriesInPack: newServer(e.GetScheduledQueriesInPack, decodeGetScheduledQueriesInPackRequest), - GetScheduledQuery: newServer(e.GetScheduledQuery, decodeGetScheduledQueryRequest), - ModifyScheduledQuery: newServer(e.ModifyScheduledQuery, decodeModifyScheduledQueryRequest), - DeleteScheduledQuery: newServer(e.DeleteScheduledQuery, decodeDeleteScheduledQueryRequest), + ApplyPackSpecs: newServer(e.ApplyPackSpecs, decodeApplyPackSpecsRequest), + GetPackSpecs: newServer(e.GetPackSpecs, decodeNoParamsRequest), + GetPackSpec: newServer(e.GetPackSpec, decodeGetGenericSpecRequest), EnrollAgent: newServer(e.EnrollAgent, decodeEnrollAgentRequest), GetClientConfig: newServer(e.GetClientConfig, decodeGetClientConfigRequest), GetDistributedQueries: newServer(e.GetDistributedQueries, decodeGetDistributedQueriesRequest), @@ -304,13 +302,10 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), GetLabel: newServer(e.GetLabel, decodeGetLabelRequest), ListLabels: newServer(e.ListLabels, decodeListLabelsRequest), - CreateLabel: newServer(e.CreateLabel, decodeCreateLabelRequest), DeleteLabel: newServer(e.DeleteLabel, decodeDeleteLabelRequest), - ModifyLabel: newServer(e.ModifyLabel, decodeModifyLabelRequest), - ListDecorators: newServer(e.ListDecorators, decodeNoParamsRequest), - NewDecorator: newServer(e.NewDecorator, decodeNewDecoratorRequest), - ModifyDecorator: newServer(e.ModifyDecorator, decodeModifyDecoratorRequest), - DeleteDecorator: newServer(e.DeleteDecorator, decodeDeleteDecoratorRequest), + ApplyLabelSpecs: newServer(e.ApplyLabelSpecs, decodeApplyLabelSpecsRequest), + GetLabelSpecs: newServer(e.GetLabelSpecs, decodeNoParamsRequest), + GetLabelSpec: newServer(e.GetLabelSpec, decodeGetGenericSpecRequest), GetHost: newServer(e.GetHost, decodeGetHostRequest), DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest), ListHosts: newServer(e.ListHosts, decodeListHostsRequest), @@ -319,7 +314,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli GetOptions: newServer(e.GetOptions, decodeNoParamsRequest), ModifyOptions: newServer(e.ModifyOptions, decodeModifyOptionsRequest), ResetOptions: newServer(e.ResetOptions, decodeNoParamsRequest), - ImportConfig: newServer(e.ImportConfig, decodeImportConfigRequest), + ApplyOsqueryOptionsSpec: newServer(e.ApplyOsqueryOptionsSpec, decodeApplyOsqueryOptionsSpecRequest), + GetOsqueryOptionsSpec: newServer(e.GetOsqueryOptionsSpec, decodeNoParamsRequest), GetCertificate: newServer(e.GetCertificate, decodeNoParamsRequest), ChangeEmail: newServer(e.ChangeEmail, decodeChangeEmailRequest), InitiateSSO: newServer(e.InitiateSSO, decodeInitiateSSORequest), @@ -406,30 +402,28 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/queries", h.ListQueries).Methods("GET").Name("list_queries") r.Handle("/api/v1/kolide/queries", h.CreateQuery).Methods("POST").Name("create_query") r.Handle("/api/v1/kolide/queries/{id}", h.ModifyQuery).Methods("PATCH").Name("modify_query") - r.Handle("/api/v1/kolide/queries/{id}", h.DeleteQuery).Methods("DELETE").Name("delete_query") + r.Handle("/api/v1/kolide/queries/{name}", h.DeleteQuery).Methods("DELETE").Name("delete_query") r.Handle("/api/v1/kolide/queries/delete", h.DeleteQueries).Methods("POST").Name("delete_queries") + r.Handle("/api/v1/kolide/spec/queries", h.ApplyQuerySpecs).Methods("POST").Name("apply_query_specs") + r.Handle("/api/v1/kolide/spec/queries", h.GetQuerySpecs).Methods("GET").Name("get_query_specs") + r.Handle("/api/v1/kolide/spec/queries/{name}", h.GetQuerySpec).Methods("GET").Name("get_query_spec") r.Handle("/api/v1/kolide/queries/run", h.CreateDistributedQueryCampaign).Methods("POST").Name("create_distributed_query_campaign") + r.Handle("/api/v1/kolide/queries/run_by_names", h.CreateDistributedQueryCampaignByNames).Methods("POST").Name("create_distributed_query_campaign_by_names") r.Handle("/api/v1/kolide/packs/{id}", h.GetPack).Methods("GET").Name("get_pack") r.Handle("/api/v1/kolide/packs", h.ListPacks).Methods("GET").Name("list_packs") - r.Handle("/api/v1/kolide/packs", h.CreatePack).Methods("POST").Name("create_pack") - r.Handle("/api/v1/kolide/packs/{id}", h.ModifyPack).Methods("PATCH").Name("modify_pack") - r.Handle("/api/v1/kolide/packs/{id}", h.DeletePack).Methods("DELETE").Name("delete_pack") + r.Handle("/api/v1/kolide/packs/{name}", h.DeletePack).Methods("DELETE").Name("delete_pack") r.Handle("/api/v1/kolide/packs/{id}/scheduled", h.GetScheduledQueriesInPack).Methods("GET").Name("get_scheduled_queries_in_pack") - r.Handle("/api/v1/kolide/schedule", h.ScheduleQuery).Methods("POST").Name("schedule_query") - r.Handle("/api/v1/kolide/schedule/{id}", h.GetScheduledQuery).Methods("GET").Name("get_scheduled_query") - r.Handle("/api/v1/kolide/schedule/{id}", h.ModifyScheduledQuery).Methods("PATCH").Name("modify_scheduled_query") - r.Handle("/api/v1/kolide/schedule/{id}", h.DeleteScheduledQuery).Methods("DELETE").Name("delete_scheduled_query") + r.Handle("/api/v1/kolide/spec/packs", h.ApplyPackSpecs).Methods("POST").Name("apply_pack_specs") + r.Handle("/api/v1/kolide/spec/packs", h.GetPackSpecs).Methods("GET").Name("get_pack_specs") + r.Handle("/api/v1/kolide/spec/packs/{name}", h.GetPackSpec).Methods("GET").Name("get_pack_spec") + r.Handle("/api/v1/kolide/labels/{id}", h.GetLabel).Methods("GET").Name("get_label") r.Handle("/api/v1/kolide/labels", h.ListLabels).Methods("GET").Name("list_labels") - r.Handle("/api/v1/kolide/labels", h.CreateLabel).Methods("POST").Name("create_label") - r.Handle("/api/v1/kolide/labels/{id}", h.DeleteLabel).Methods("DELETE").Name("delete_label") - r.Handle("/api/v1/kolide/labels/{id}", h.ModifyLabel).Methods("PATCH").Name("modify_label") - - r.Handle("/api/v1/kolide/decorators", h.ListDecorators).Methods("GET").Name("list_decorators") - r.Handle("/api/v1/kolide/decorators", h.NewDecorator).Methods("POST").Name("create_decorator") - r.Handle("/api/v1/kolide/decorators/{id}", h.ModifyDecorator).Methods("PATCH").Name("modify_decorator") - r.Handle("/api/v1/kolide/decorators/{id}", h.DeleteDecorator).Methods("DELETE").Name("delete_decorator") + r.Handle("/api/v1/kolide/labels/{name}", h.DeleteLabel).Methods("DELETE").Name("delete_label") + r.Handle("/api/v1/kolide/spec/labels", h.ApplyLabelSpecs).Methods("POST").Name("apply_label_specs") + r.Handle("/api/v1/kolide/spec/labels", h.GetLabelSpecs).Methods("GET").Name("get_label_specs") + r.Handle("/api/v1/kolide/spec/labels/{name}", h.GetLabelSpec).Methods("GET").Name("get_label_spec") r.Handle("/api/v1/kolide/hosts", h.ListHosts).Methods("GET").Name("list_hosts") r.Handle("/api/v1/kolide/host_summary", h.GetHostSummary).Methods("GET").Name("get_host_summary") @@ -442,11 +436,11 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/options", h.GetOptions).Methods("GET").Name("get_options") r.Handle("/api/v1/kolide/options", h.ModifyOptions).Methods("PATCH").Name("modify_options") r.Handle("/api/v1/kolide/options/reset", h.ResetOptions).Methods("GET").Name("reset_options") + r.Handle("/api/v1/kolide/spec/osquery_options", h.ApplyOsqueryOptionsSpec).Methods("POST").Name("apply_osquery_options_spec") + r.Handle("/api/v1/kolide/spec/osquery_options", h.GetOsqueryOptionsSpec).Methods("GET").Name("get_osquery_options_spec") r.Handle("/api/v1/kolide/targets", h.SearchTargets).Methods("POST").Name("search_targets") - r.Handle("/api/v1/kolide/osquery/config/import", h.ImportConfig).Methods("POST").Name("import_config") - 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") diff --git a/server/service/handler_test.go b/server/service/handler_test.go index a3f92a643..e8f923eb1 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -133,18 +133,6 @@ func TestAPIRoutes(t *testing.T) { verb: "GET", uri: "/api/v1/kolide/packs/1/scheduled", }, - { - verb: "POST", - uri: "/api/v1/kolide/schedule", - }, - { - verb: "DELETE", - uri: "/api/v1/kolide/schedule/1", - }, - { - verb: "PATCH", - uri: "/api/v1/kolide/schedule/1", - }, { verb: "POST", uri: "/api/v1/osquery/enroll", diff --git a/server/service/logging_decorators.go b/server/service/logging_decorators.go deleted file mode 100644 index bd370a0ce..000000000 --- a/server/service/logging_decorators.go +++ /dev/null @@ -1,69 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/kolide/fleet/server/kolide" -) - -func (mw loggingMiddleware) ListDecorators(ctx context.Context) ([]*kolide.Decorator, error) { - var ( - decs []*kolide.Decorator - err error - ) - defer func(begin time.Time) { - mw.logger.Log( - "method", "ListDecorators", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - decs, err = mw.Service.ListDecorators(ctx) - return decs, err -} - -func (mw loggingMiddleware) NewDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - var ( - dec *kolide.Decorator - err error - ) - defer func(begin time.Time) { - mw.logger.Log( - "method", "NewDecorator", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - dec, err = mw.Service.NewDecorator(ctx, payload) - return dec, err -} - -func (mw loggingMiddleware) ModifyDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - var ( - dec *kolide.Decorator - err error - ) - defer func(begin time.Time) { - mw.logger.Log( - "method", "ModifyDecorator", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - dec, err = mw.Service.ModifyDecorator(ctx, payload) - return dec, err -} - -func (mw loggingMiddleware) DeleteDecorator(ctx context.Context, id uint) error { - var err error - defer func(begin time.Time) { - mw.logger.Log( - "method", "DeleteDecorator", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - err = mw.Service.DeleteDecorator(ctx, id) - return err -} diff --git a/server/service/logging_import_config.go b/server/service/logging_import_config.go deleted file mode 100644 index 33d72dcba..000000000 --- a/server/service/logging_import_config.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/kolide/fleet/server/kolide" -) - -func (mw loggingMiddleware) ImportConfig(ctx context.Context, cfg *kolide.ImportConfig) (*kolide.ImportConfigResponse, error) { - var ( - resp *kolide.ImportConfigResponse - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "ImportConfig", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - resp, err = mw.Service.ImportConfig(ctx, cfg) - return resp, err - -} diff --git a/server/service/logging_labels.go b/server/service/logging_labels.go index c91f28d50..de2d273a5 100644 --- a/server/service/logging_labels.go +++ b/server/service/logging_labels.go @@ -7,24 +7,6 @@ import ( "github.com/kolide/fleet/server/kolide" ) -func (mw loggingMiddleware) ModifyLabel(ctx context.Context, id uint, p kolide.ModifyLabelPayload) (*kolide.Label, error) { - var ( - label *kolide.Label - err error - ) - - defer func(begin time.Time) { - mw.logger.Log( - "method", "ModifyLabel", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - label, err = mw.Service.ModifyLabel(ctx, id, p) - return label, err -} - func (mw loggingMiddleware) ListLabels(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Label, error) { var ( labels []*kolide.Label @@ -61,25 +43,7 @@ func (mw loggingMiddleware) GetLabel(ctx context.Context, id uint) (*kolide.Labe return label, err } -func (mw loggingMiddleware) NewLabel(ctx context.Context, p kolide.LabelPayload) (*kolide.Label, error) { - var ( - label *kolide.Label - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "NewLabel", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - label, err = mw.Service.NewLabel(ctx, p) - return label, err -} - -func (mw loggingMiddleware) DeleteLabel(ctx context.Context, id uint) error { +func (mw loggingMiddleware) DeleteLabel(ctx context.Context, name string) error { var ( err error ) @@ -92,6 +56,42 @@ func (mw loggingMiddleware) DeleteLabel(ctx context.Context, id uint) error { ) }(time.Now()) - err = mw.Service.DeleteLabel(ctx, id) + err = mw.Service.DeleteLabel(ctx, name) + return err +} + +func (mw loggingMiddleware) GetLabelSpec(ctx context.Context, name string) (spec *kolide.LabelSpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetLabelSpec", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + spec, err = mw.Service.GetLabelSpec(ctx, name) + return spec, err +} + +func (mw loggingMiddleware) GetLabelSpecs(ctx context.Context) (specs []*kolide.LabelSpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetLabelSpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + specs, err = mw.Service.GetLabelSpecs(ctx) + return specs, err +} + +func (mw loggingMiddleware) ApplyLabelSpecs(ctx context.Context, specs []*kolide.LabelSpec) (err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "ApplyLabelSpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + err = mw.Service.ApplyLabelSpecs(ctx, specs) return err } diff --git a/server/service/logging_osquery.go b/server/service/logging_osquery.go index 023b63bc0..0f3d7cbb4 100644 --- a/server/service/logging_osquery.go +++ b/server/service/logging_osquery.go @@ -48,9 +48,9 @@ func (mw loggingMiddleware) AuthenticateHost(ctx context.Context, nodeKey string return host, err } -func (mw loggingMiddleware) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, error) { +func (mw loggingMiddleware) GetClientConfig(ctx context.Context) (map[string]interface{}, error) { var ( - config *kolide.OsqueryConfig + config map[string]interface{} err error ) diff --git a/server/service/logging_osquery_options.go b/server/service/logging_osquery_options.go new file mode 100644 index 000000000..96b5e67e0 --- /dev/null +++ b/server/service/logging_osquery_options.go @@ -0,0 +1,32 @@ +package service + +import ( + "context" + "time" + + "github.com/kolide/fleet/server/kolide" +) + +func (mw loggingMiddleware) GetOptionsSpec(ctx context.Context) (spec *kolide.OptionsSpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetOptionsSpec", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + spec, err = mw.Service.GetOptionsSpec(ctx) + return spec, err +} + +func (mw loggingMiddleware) ApplyOptionsSpec(ctx context.Context, spec *kolide.OptionsSpec) (err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "ApplyOptionsSpec", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + err = mw.Service.ApplyOptionsSpec(ctx, spec) + return err +} diff --git a/server/service/logging_packs.go b/server/service/logging_packs.go index 46e54e160..786545db7 100644 --- a/server/service/logging_packs.go +++ b/server/service/logging_packs.go @@ -43,43 +43,7 @@ func (mw loggingMiddleware) GetPack(ctx context.Context, id uint) (*kolide.Pack, return pack, err } -func (mw loggingMiddleware) NewPack(ctx context.Context, p kolide.PackPayload) (*kolide.Pack, error) { - var ( - pack *kolide.Pack - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "NewPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - pack, err = mw.Service.NewPack(ctx, p) - return pack, err -} - -func (mw loggingMiddleware) ModifyPack(ctx context.Context, id uint, p kolide.PackPayload) (*kolide.Pack, error) { - var ( - pack *kolide.Pack - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "ModifyPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - pack, err = mw.Service.ModifyPack(ctx, id, p) - return pack, err -} - -func (mw loggingMiddleware) DeletePack(ctx context.Context, id uint) error { +func (mw loggingMiddleware) DeletePack(ctx context.Context, name string) error { var ( err error ) @@ -92,41 +56,7 @@ func (mw loggingMiddleware) DeletePack(ctx context.Context, id uint) error { ) }(time.Now()) - err = mw.Service.DeletePack(ctx, id) - return err -} - -func (mw loggingMiddleware) AddLabelToPack(ctx context.Context, lid uint, pid uint) error { - var ( - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "AddLabelToPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - err = mw.Service.AddLabelToPack(ctx, lid, pid) - return err -} - -func (mw loggingMiddleware) RemoveLabelFromPack(ctx context.Context, lid uint, pid uint) error { - var ( - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "RemoveLabelFromPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - err = mw.Service.RemoveLabelFromPack(ctx, lid, pid) + err = mw.Service.DeletePack(ctx, name) return err } @@ -148,40 +78,6 @@ func (mw loggingMiddleware) ListLabelsForPack(ctx context.Context, pid uint) ([] return labels, err } -func (mw loggingMiddleware) AddHostToPack(ctx context.Context, hid uint, pid uint) error { - var ( - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "AddHostToPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - err = mw.Service.AddHostToPack(ctx, hid, pid) - return err -} - -func (mw loggingMiddleware) RemoveHostFromPack(ctx context.Context, hid uint, pid uint) error { - var ( - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "RemoveHostFromPack", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - err = mw.Service.RemoveHostFromPack(ctx, hid, pid) - return err -} - func (mw loggingMiddleware) ListPacksForHost(ctx context.Context, hid uint) ([]*kolide.Pack, error) { var ( packs []*kolide.Pack @@ -217,3 +113,39 @@ func (mw loggingMiddleware) ListHostsInPack(ctx context.Context, pid uint, opt k hosts, err = mw.Service.ListHostsInPack(ctx, pid, opt) return hosts, err } + +func (mw loggingMiddleware) GetPackSpec(ctx context.Context, name string) (spec *kolide.PackSpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetPackSpec", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + spec, err = mw.Service.GetPackSpec(ctx, name) + return spec, err +} + +func (mw loggingMiddleware) GetPackSpecs(ctx context.Context) (specs []*kolide.PackSpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetPackSpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + specs, err = mw.Service.GetPackSpecs(ctx) + return specs, err +} + +func (mw loggingMiddleware) ApplyPackSpecs(ctx context.Context, specs []*kolide.PackSpec) (err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "ApplyPackSpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + err = mw.Service.ApplyPackSpecs(ctx, specs) + return err +} diff --git a/server/service/logging_queries.go b/server/service/logging_queries.go new file mode 100644 index 000000000..0972cc80b --- /dev/null +++ b/server/service/logging_queries.go @@ -0,0 +1,44 @@ +package service + +import ( + "context" + "time" + + "github.com/kolide/fleet/server/kolide" +) + +func (mw loggingMiddleware) GetQuerySpec(ctx context.Context, name string) (spec *kolide.QuerySpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetQuerySpec", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + spec, err = mw.Service.GetQuerySpec(ctx, name) + return spec, err +} + +func (mw loggingMiddleware) GetQuerySpecs(ctx context.Context) (specs []*kolide.QuerySpec, err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "GetQuerySpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + specs, err = mw.Service.GetQuerySpecs(ctx) + return specs, err +} + +func (mw loggingMiddleware) ApplyQuerySpecs(ctx context.Context, specs []*kolide.QuerySpec) (err error) { + defer func(begin time.Time) { + mw.logger.Log( + "method", "ApplyQuerySpecs", + "err", err, + "took", time.Since(begin), + ) + }(time.Now()) + err = mw.Service.ApplyQuerySpecs(ctx, specs) + return err +} diff --git a/server/service/logging_scheduled_queries.go b/server/service/logging_scheduled_queries.go index 64023ba8f..7d1e7d95c 100644 --- a/server/service/logging_scheduled_queries.go +++ b/server/service/logging_scheduled_queries.go @@ -7,24 +7,6 @@ import ( "github.com/kolide/fleet/server/kolide" ) -func (mw loggingMiddleware) GetScheduledQuery(ctx context.Context, id uint) (*kolide.ScheduledQuery, error) { - var ( - query *kolide.ScheduledQuery - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "GetScheduledQuery", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - query, err = mw.Service.GetScheduledQuery(ctx, id) - return query, err -} - func (mw loggingMiddleware) GetScheduledQueriesInPack(ctx context.Context, id uint, opts kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { var ( queries []*kolide.ScheduledQuery @@ -42,56 +24,3 @@ func (mw loggingMiddleware) GetScheduledQueriesInPack(ctx context.Context, id ui queries, err = mw.Service.GetScheduledQueriesInPack(ctx, id, opts) return queries, err } - -func (mw loggingMiddleware) ScheduleQuery(ctx context.Context, sq *kolide.ScheduledQuery) (*kolide.ScheduledQuery, error) { - var ( - query *kolide.ScheduledQuery - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "ScheduleQuery", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - query, err = mw.Service.ScheduleQuery(ctx, sq) - return query, err -} - -func (mw loggingMiddleware) DeleteScheduledQuery(ctx context.Context, id uint) error { - var ( - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "DeleteScheduledQuery", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - err = mw.Service.DeleteScheduledQuery(ctx, id) - return err -} - -func (mw loggingMiddleware) ModifyScheduledQuery(ctx context.Context, id uint, p kolide.ScheduledQueryPayload) (*kolide.ScheduledQuery, error) { - var ( - query *kolide.ScheduledQuery - err error - ) - - defer func(begin time.Time) { - _ = mw.logger.Log( - "method", "ModifyScheduledQuery", - "err", err, - "took", time.Since(begin), - ) - }(time.Now()) - - query, err = mw.Service.ModifyScheduledQuery(ctx, id, p) - return query, err -} diff --git a/server/service/metrics_decorators.go b/server/service/metrics_decorators.go deleted file mode 100644 index 60b44ffaf..000000000 --- a/server/service/metrics_decorators.go +++ /dev/null @@ -1,62 +0,0 @@ -package service - -import ( - "context" - "fmt" - "time" - - "github.com/kolide/fleet/server/kolide" -) - -func (mw metricsMiddleware) ListDecorators(ctx context.Context) ([]*kolide.Decorator, error) { - var ( - decs []*kolide.Decorator - err error - ) - defer func(begin time.Time) { - lvs := []string{"method", "ListDecorators", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - decs, err = mw.Service.ListDecorators(ctx) - return decs, err -} - -func (mw metricsMiddleware) NewDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - var ( - dec *kolide.Decorator - err error - ) - defer func(begin time.Time) { - lvs := []string{"method", "NewDecorator", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - dec, err = mw.Service.NewDecorator(ctx, payload) - return dec, err -} - -func (mw metricsMiddleware) ModifyDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - var ( - dec *kolide.Decorator - err error - ) - defer func(begin time.Time) { - lvs := []string{"method", "ModifyDecorator", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - dec, err = mw.Service.ModifyDecorator(ctx, payload) - return dec, err -} - -func (mw metricsMiddleware) DeleteDecorator(ctx context.Context, id uint) error { - var err error - defer func(begin time.Time) { - lvs := []string{"method", "DeleteDecorator", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - err = mw.Service.DeleteDecorator(ctx, id) - return err -} diff --git a/server/service/metrics_import_config.go b/server/service/metrics_import_config.go deleted file mode 100644 index 63d5d4ea8..000000000 --- a/server/service/metrics_import_config.go +++ /dev/null @@ -1,23 +0,0 @@ -package service - -import ( - "context" - "fmt" - "time" - - "github.com/kolide/fleet/server/kolide" -) - -func (mw metricsMiddleware) ImportConfig(ctx context.Context, cfg *kolide.ImportConfig) (*kolide.ImportConfigResponse, error) { - var ( - resp *kolide.ImportConfigResponse - err error - ) - defer func(begin time.Time) { - lvs := []string{"method", "ImportConfig", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - resp, err = mw.Service.ImportConfig(ctx, cfg) - return resp, err -} diff --git a/server/service/metrics_labels.go b/server/service/metrics_labels.go deleted file mode 100644 index 6cfe31098..000000000 --- a/server/service/metrics_labels.go +++ /dev/null @@ -1,24 +0,0 @@ -package service - -import ( - "context" - "fmt" - "time" - - "github.com/kolide/fleet/server/kolide" -) - -func (mw metricsMiddleware) ModifyLabel(ctx context.Context, id uint, p kolide.ModifyLabelPayload) (*kolide.Label, error) { - var ( - lic *kolide.Label - err error - ) - defer func(begin time.Time) { - lvs := []string{"method", "ModifyLabel", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(lvs...).Add(1) - mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - lic, err = mw.Service.ModifyLabel(ctx, id, p) - return lic, err - -} diff --git a/server/service/service_campaigns.go b/server/service/service_campaigns.go index 08e8489ec..1f7948ac8 100644 --- a/server/service/service_campaigns.go +++ b/server/service/service_campaigns.go @@ -11,6 +11,20 @@ import ( "github.com/pkg/errors" ) +func (svc service) NewDistributedQueryCampaignByNames(ctx context.Context, queryString string, hosts []string, labels []string) (*kolide.DistributedQueryCampaign, error) { + hostIDs, err := svc.ds.HostIDsByName(hosts) + if err != nil { + return nil, errors.Wrap(err, "finding host IDs") + } + + labelIDs, err := svc.ds.LabelIDsByName(labels) + if err != nil { + return nil, errors.Wrap(err, "finding label IDs") + } + + return svc.NewDistributedQueryCampaign(ctx, queryString, hostIDs, labelIDs) +} + func (svc service) NewDistributedQueryCampaign(ctx context.Context, queryString string, hosts []uint, labels []uint) (*kolide.DistributedQueryCampaign, error) { vc, ok := viewer.FromContext(ctx) if !ok { @@ -121,8 +135,8 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co status := campaignStatus{ Status: campaignStatusPending, } - - lastStatus := status.Status + lastStatus := status + lastTotals := targetTotals{} // to improve performance of the frontend rendering the results table, we // add the "host_hostname" field to every row. @@ -157,8 +171,11 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co Offline: metrics.OfflineHosts, MissingInAction: metrics.MissingInActionHosts, } - if err = conn.WriteJSONMessage("totals", totals); err != nil { - return + if lastTotals != totals { + lastTotals = totals + if err = conn.WriteJSONMessage("totals", totals); err != nil { + return + } } status.ExpectedResults = totals.Online @@ -166,8 +183,8 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co status.Status = campaignStatusFinished } // only write status message if status has changed - if lastStatus != status.Status { - lastStatus = status.Status + if lastStatus != status { + lastStatus = status if err = conn.WriteJSONMessage("status", status); err != nil { return } diff --git a/server/service/service_decorators.go b/server/service/service_decorators.go deleted file mode 100644 index 403970649..000000000 --- a/server/service/service_decorators.go +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import ( - "context" - - "github.com/kolide/fleet/server/kolide" -) - -func (svc service) ListDecorators(ctx context.Context) ([]*kolide.Decorator, error) { - return svc.ds.ListDecorators() -} - -func (svc service) DeleteDecorator(ctx context.Context, uid uint) error { - return svc.ds.DeleteDecorator(uid) -} - -func (svc service) NewDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - var dec kolide.Decorator - if payload.Name != nil { - dec.Name = *payload.Name - } - dec.Query = *payload.Query - dec.Type = *payload.DecoratorType - if payload.Interval != nil { - dec.Interval = *payload.Interval - } - return svc.ds.NewDecorator(&dec) -} - -func (svc service) ModifyDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - dec, err := svc.ds.Decorator(payload.ID) - if err != nil { - return nil, err - } - if payload.Name != nil { - dec.Name = *payload.Name - } - if payload.DecoratorType != nil { - dec.Type = *payload.DecoratorType - } - if payload.Query != nil { - dec.Query = *payload.Query - } - if payload.Interval != nil { - dec.Interval = *payload.Interval - } - err = svc.ds.SaveDecorator(dec) - if err != nil { - return nil, err - } - return dec, nil -} diff --git a/server/service/service_fim.go b/server/service/service_fim.go index 1e5b357ec..15ab66e5e 100644 --- a/server/service/service_fim.go +++ b/server/service/service_fim.go @@ -2,10 +2,10 @@ package service import ( "context" + "encoding/json" "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" - "encoding/json" ) func (svc service) GetFIM(ctx context.Context) (*kolide.FIMConfig, error) { @@ -26,8 +26,8 @@ func (svc service) GetFIM(ctx context.Context) (*kolide.FIMConfig, error) { } result := &kolide.FIMConfig{ - Interval: uint(config.FIMInterval), - FilePaths: paths, + Interval: uint(config.FIMInterval), + FilePaths: paths, FileAccesses: arr, } return result, nil diff --git a/server/service/service_fim_test.go b/server/service/service_fim_test.go index 5cb3e530c..c89f85278 100644 --- a/server/service/service_fim_test.go +++ b/server/service/service_fim_test.go @@ -19,7 +19,7 @@ func TestGetFIMService(t *testing.T) { AppConfigStore: mock.AppConfigStore{ AppConfigFunc: func() (*kolide.AppConfig, error) { config := &kolide.AppConfig{ - FIMInterval: fimIntervalTestValue, + FIMInterval: fimIntervalTestValue, FIMFileAccesses: fileAccessesString, } return config, nil @@ -59,7 +59,7 @@ func TestUpdateFIM(t *testing.T) { AppConfigStore: mock.AppConfigStore{ AppConfigFunc: func() (*kolide.AppConfig, error) { config := &kolide.AppConfig{ - FIMInterval: fimIntervalTestValue, + FIMInterval: fimIntervalTestValue, FIMFileAccesses: fileAccessesString, } return config, nil @@ -82,7 +82,7 @@ func TestUpdateFIM(t *testing.T) { ds: ds, } fim := kolide.FIMConfig{ - Interval: uint(fimIntervalTestValue), + Interval: uint(fimIntervalTestValue), FileAccesses: fileAccessStringValue, FilePaths: kolide.FIMSections{ "etc": []string{ diff --git a/server/service/service_import_config.go b/server/service/service_import_config.go deleted file mode 100644 index ab62f1fb7..000000000 --- a/server/service/service_import_config.go +++ /dev/null @@ -1,533 +0,0 @@ -package service - -import ( - "context" - "crypto/md5" - "fmt" - "strconv" - "strings" - - "github.com/kolide/fleet/server/contexts/viewer" - "github.com/kolide/fleet/server/kolide" - "github.com/pkg/errors" -) - -func (svc service) ImportConfig(ctx context.Context, cfg *kolide.ImportConfig) (*kolide.ImportConfigResponse, error) { - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - vc, ok := viewer.FromContext(ctx) - if !ok { - return nil, errors.New("internal error, unable to fetch user") - } - tx, err := svc.ds.Begin() - if err != nil { - return nil, err - } - - if err := svc.importOptions(cfg.Options, resp, tx); err != nil { - svc.rollbackImportConfig(tx, "importOptions") - return nil, errors.Wrap(err, "importOptions failed") - } - if err := svc.importPacks(vc.UserID(), cfg, resp, tx); err != nil { - svc.rollbackImportConfig(tx, "importPacks") - return nil, errors.Wrap(err, "importPacks failed") - } - if err := svc.importScheduledQueries(vc.UserID(), cfg, resp, tx); err != nil { - svc.rollbackImportConfig(tx, "importScheduledQueries") - return nil, errors.Wrap(err, "importScheduledQueries failed") - } - if err := svc.importDecorators(cfg, resp, tx); err != nil { - svc.rollbackImportConfig(tx, "importDecorators") - return nil, errors.Wrap(err, "importDecorators") - } - if err := svc.importFIMSections(cfg, resp, tx); err != nil { - svc.rollbackImportConfig(tx, "importFIMSections") - return nil, errors.Wrap(err, "importFIMSections") - } - if cfg.DryRun { - if err := tx.Rollback(); err != nil { - return nil, errors.Wrap(err, "dry run rollback failed") - } - return resp, nil - } - if err := tx.Commit(); err != nil { - return nil, errors.Wrap(err, "commit failed") - } - return resp, nil -} - -func (svc service) rollbackImportConfig(tx kolide.Transaction, method string) { - if err := tx.Rollback(); err != nil { - svc.logger.Log( - "method", method, - "err", errors.Wrap(err, fmt.Sprintf("db rollback failed in %s", method)), - ) - } -} - -func (svc service) importYARA(cfg *kolide.ImportConfig, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - if cfg.YARA != nil { - for sig, paths := range cfg.YARA.Signatures { - ysg := &kolide.YARASignatureGroup{ - SignatureName: sig, - Paths: paths, - } - _, err := svc.ds.NewYARASignatureGroup(ysg, kolide.HasTransaction(tx)) - if _, ok := err.(dbDuplicateError); ok { - resp.Status(kolide.YARAFileSection).SkipCount++ - resp.Status(kolide.YARAFileSection).Warning(kolide.YARADuplicate, "skipped '%s', already exists", sig) - continue - } - if err != nil { - return err - } - resp.Status(kolide.YARASigSection).ImportCount++ - resp.Status(kolide.YARASigSection).Message("imported '%s'", sig) - } - for section, sigs := range cfg.YARA.FilePaths { - for _, sig := range sigs { - err := svc.ds.NewYARAFilePath(section, sig, kolide.HasTransaction(tx)) - if _, ok := err.(dbDuplicateError); ok { - resp.Status(kolide.YARAFileSection).SkipCount++ - resp.Status(kolide.YARAFileSection).Warning(kolide.YARADuplicate, "skipped '%s', already exists", section) - continue - } - if err != nil { - return err - } - resp.Status(kolide.YARAFileSection).ImportCount++ - resp.Status(kolide.YARAFileSection).Message("imported '%s'", section) - } - } - } - return nil -} - -type dbDuplicateError interface { - IsExists() bool -} - -func (svc service) importFIMSections(cfg *kolide.ImportConfig, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - if cfg.FileIntegrityMonitoring != nil { - for sectionName, paths := range cfg.FileIntegrityMonitoring { - fp := &kolide.FIMSection{ - SectionName: sectionName, - Description: "imported", - Paths: paths, - } - _, err := svc.ds.NewFIMSection(fp, kolide.HasTransaction(tx)) - if _, ok := err.(dbDuplicateError); ok { - resp.Status(kolide.FilePathsSection).SkipCount++ - resp.Status(kolide.FilePathsSection).Warning(kolide.FIMDuplicate, "skipped '%s', already exists", sectionName) - continue - } - if err != nil { - return err - } - resp.Status(kolide.FilePathsSection).ImportCount++ - resp.Status(kolide.FilePathsSection).Message("imported '%s'", sectionName) - } - } - // this has to happen AFTER fim section, because it requires file paths - return svc.importYARA(cfg, resp, tx) -} - -func (svc service) getExistingDecoratorQueries(tx kolide.Transaction) (map[string]int, error) { - decs, err := svc.ds.ListDecorators(kolide.HasTransaction(tx)) - if err != nil { - return nil, err - } - queryHashes := map[string]int{} - for _, dec := range decs { - hash := fmt.Sprintf("%x", md5.Sum([]byte(dec.Query))) - queryHashes[hash] = 0 - } - return queryHashes, nil -} - -func decoratorExists(query string, queryHashes map[string]int) bool { - hash := fmt.Sprintf("%x", md5.Sum([]byte(query))) - _, exists := queryHashes[hash] - return exists -} - -func (svc service) importDecorators(cfg *kolide.ImportConfig, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - if cfg.Decorators != nil { - queryHashes, err := svc.getExistingDecoratorQueries(tx) - if err != nil { - return errors.Wrap(err, "getting existing queries") - } - - for _, query := range cfg.Decorators.Load { - if decoratorExists(query, queryHashes) { - resp.Status(kolide.DecoratorsSection).SkipCount++ - resp.Status(kolide.DecoratorsSection).Warning(kolide.QueryDuplicate, "skipped load '%s'", query) - continue - } - decName, err := uniqueImportName() - if err != nil { - return err - } - decorator := &kolide.Decorator{ - Name: decName, - Query: query, - Type: kolide.DecoratorLoad, - } - _, err = svc.ds.NewDecorator(decorator, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.DecoratorsSection).ImportCount++ - resp.Status(kolide.DecoratorsSection).Warning("imported load '%s'", query) - } - for _, query := range cfg.Decorators.Always { - if decoratorExists(query, queryHashes) { - resp.Status(kolide.DecoratorsSection).SkipCount++ - resp.Status(kolide.DecoratorsSection).Warning(kolide.QueryDuplicate, "skipped always '%s'", query) - continue - } - decName, err := uniqueImportName() - if err != nil { - return err - } - decorator := &kolide.Decorator{ - Name: decName, - Query: query, - Type: kolide.DecoratorAlways, - } - _, err = svc.ds.NewDecorator(decorator, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.DecoratorsSection).ImportCount++ - resp.Status(kolide.DecoratorsSection).Message("imported always '%s'", query) - } - - for key, queries := range cfg.Decorators.Interval { - for _, query := range queries { - if decoratorExists(query, queryHashes) { - resp.Status(kolide.DecoratorsSection).SkipCount++ - resp.Status(kolide.DecoratorsSection).Warning(kolide.QueryDuplicate, "skipped interval '%s'", query) - continue - } - interval, err := strconv.ParseInt(key, 10, 32) - if err != nil { - return err - } - decName, err := uniqueImportName() - if err != nil { - return err - } - decorator := &kolide.Decorator{ - Name: decName, - Query: query, - Type: kolide.DecoratorInterval, - Interval: uint(interval), - } - _, err = svc.ds.NewDecorator(decorator, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.DecoratorsSection).ImportCount++ - resp.Status(kolide.DecoratorsSection).Message("imported interval %d '%s'", interval, query) - } - } - - } - return nil -} - -func (svc service) importScheduledQueries(uid uint, cfg *kolide.ImportConfig, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - _, ok, err := svc.ds.PackByName(kolide.ImportPackName, kolide.HasTransaction(tx)) - if ok { - resp.Status(kolide.PacksSection).Warning( - kolide.PackDuplicate, "skipped '%s' already exists", kolide.ImportPackName, - ) - resp.Status(kolide.PacksSection).SkipCount++ - return nil - } - // create import pack to hold imported scheduled queries - pack := &kolide.Pack{ - Name: kolide.ImportPackName, - Description: "holds imported scheduled queries", - CreatedBy: uid, - Disabled: false, - } - pack, err = svc.ds.NewPack(pack, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.PacksSection).ImportCount++ - resp.Status(kolide.PacksSection).Message("created import pack") - - for queryName, queryDetails := range cfg.Schedule { - var query *kolide.Query - query, ok, err = svc.ds.QueryByName(queryName, kolide.HasTransaction(tx)) - // if we find the query check to see if the import query matches the - // query we have, if it doesn't skip it - if ok { - if hashQuery("", query.Query) != hashQuery("", queryDetails.Query) { - resp.Status(kolide.PacksSection).Warning( - kolide.DifferentQuerySameName, - "queries named '%s' have different statements and won't be added to '%s'", - queryName, - pack.Name, - ) - continue - } - resp.Status(kolide.QueriesSection).Warning( - kolide.QueryDuplicate, "skipped '%s' different query of same name already exists", queryName, - ) - resp.Status(kolide.QueriesSection).SkipCount++ - } else { - // if query doesn't exist, create it - query = &kolide.Query{ - Name: queryName, - Description: "imported", - Query: queryDetails.Query, - Saved: true, - AuthorID: uid, - } - query, err = svc.ds.NewQuery(query, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.QueriesSection).ImportCount++ - resp.Status(kolide.QueriesSection).Message( - "imported scheduled query '%s'", query.Name, - ) - } - sq := &kolide.ScheduledQuery{ - PackID: pack.ID, - QueryID: query.ID, - Interval: uint(queryDetails.Interval), - Snapshot: queryDetails.Snapshot, - Removed: queryDetails.Removed, - Platform: queryDetails.Platform, - Version: queryDetails.Version, - Shard: configInt2Ptr(queryDetails.Shard), - } - _, err = svc.ds.NewScheduledQuery(sq, kolide.HasTransaction(tx)) - if err != nil { - return nil - } - resp.Status(kolide.PacksSection).Message( - "added query '%s' to '%s'", query.Name, pack.Name, - ) - } - return nil -} - -func (svc service) importPacks(uid uint, cfg *kolide.ImportConfig, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - labelCache := map[string]*kolide.Label{} - packs, err := cfg.CollectPacks() - if err != nil { - return err - } - for packName, packDetails := range packs { - _, ok, err := svc.ds.PackByName(packName, kolide.HasTransaction(tx)) - if err != nil { - return err - } - if ok { - resp.Status(kolide.PacksSection).Warning( - kolide.PackDuplicate, "skipped '%s' already exists", packName, - ) - resp.Status(kolide.PacksSection).SkipCount++ - continue - } - // import new pack - if packDetails.Shard != nil { - resp.Status(kolide.PacksSection).Warning( - kolide.Unsupported, - "shard for pack '%s'", - packName, - ) - } - if packDetails.Version != nil { - resp.Status(kolide.PacksSection).Warning( - kolide.Unsupported, - "version for pack '%s'", - packName, - ) - } - pack := &kolide.Pack{ - Name: packName, - Description: "Imported pack", - Platform: packDetails.Platform, - } - pack, err = svc.ds.NewPack(pack, kolide.HasTransaction(tx)) - if err != nil { - return err - } - err = svc.createLabelsForPack(pack, &packDetails, labelCache, resp, tx) - if err != nil { - return err - } - err = svc.createQueriesForPack(uid, pack, &packDetails, resp, tx) - if err != nil { - return err - } - resp.Status(kolide.PacksSection).ImportCount++ - resp.Status(kolide.PacksSection).Message("imported '%s'", packName) - } - return nil -} - -func hashQuery(platform, query string) string { - s := strings.Replace(query, " ", "", -1) - s = strings.Replace(s, "\t", "", -1) - s = strings.Replace(s, "\n", "", -1) - s = strings.Trim(s, ";") - s = platform + s - return strings.ToLower(s) -} - -func uniqueImportName() (string, error) { - random, err := kolide.RandomText(6) - if err != nil { - return "", err - } - return "import_" + random, nil -} - -func (svc service) createQueriesForPack(uid uint, pack *kolide.Pack, details *kolide.PackDetails, - resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - for queryName, queryDetails := range details.Queries { - query, ok, err := svc.ds.QueryByName(queryName, kolide.HasTransaction(tx)) - if err != nil { - return err - } - // if the query isn't already in the database, create it - if !ok { - query = &kolide.Query{ - Name: queryName, - Description: "imported", - Query: queryDetails.Query, - Saved: true, - AuthorID: uid, - } - query, err = svc.ds.NewQuery(query, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.QueriesSection).Message( - "created '%s' as part of pack '%s'", queryName, pack.Name, - ) - resp.Status(kolide.QueriesSection).ImportCount++ - } - // associate query with pack - scheduledQuery := &kolide.ScheduledQuery{ - PackID: pack.ID, - QueryID: query.ID, - Interval: uint(queryDetails.Interval), - Platform: queryDetails.Platform, - Snapshot: queryDetails.Snapshot, - Removed: queryDetails.Removed, - Version: queryDetails.Version, - Shard: configInt2Ptr(queryDetails.Shard), - } - _, err = svc.ds.NewScheduledQuery(scheduledQuery, kolide.HasTransaction(tx)) - if err != nil { - return nil - } - resp.Status(kolide.PacksSection).Message("added query '%s'", query.Name) - - } - return nil -} - -// createLabelsForPack Iterates through discover queries, creates a label for -// each query and assigns it to the pack passed as an argument. Once a Label is created we cache -// it for reuse. -func (svc service) createLabelsForPack(pack *kolide.Pack, details *kolide.PackDetails, - cache map[string]*kolide.Label, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - for _, query := range details.Discovery { - hash := hashQuery(details.Platform, query) - label, ok := cache[hash] - // add existing label to pack - if ok { - err := svc.ds.AddLabelToPack(label.ID, pack.ID, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.PacksSection).Message( - "added label '%s' to pack '%s'", label.Name, pack.Name, - ) - continue - } - // create new label and add it to pack - labelName, err := uniqueImportName() - if err != nil { - return err - } - label = &kolide.Label{ - Name: labelName, - Query: query, - Description: "imported", - LabelType: kolide.LabelTypeRegular, - Platform: details.Platform, - } - label, err = svc.ds.NewLabel(label, kolide.HasTransaction(tx)) - if err != nil { - return err - } - // hang on to label so we can reuse it for other packs if needed - cache[hash] = label - err = svc.ds.AddLabelToPack(label.ID, pack.ID, kolide.HasTransaction(tx)) - if err != nil { - return err - } - resp.Status(kolide.PacksSection).Message( - "added label '%s' to '%s'", label.Name, pack.Name, - ) - } - return nil -} - -func (svc service) importOptions(opts kolide.OptionNameToValueMap, resp *kolide.ImportConfigResponse, tx kolide.Transaction) error { - var updateOptions []kolide.Option - for optName, optValue := range opts { - opt, err := svc.ds.OptionByName(optName, kolide.HasTransaction(tx)) - if err != nil { - resp.Status(kolide.OptionsSection).Warning( - kolide.OptionUnknown, "skipped '%s' can't find option", optName, - ) - resp.Status(kolide.OptionsSection).SkipCount++ - continue - } - if opt.ReadOnly { - resp.Status(kolide.OptionsSection).Warning( - kolide.OptionReadonly, "skipped '%s' can't change read only option", optName, - ) - resp.Status(kolide.OptionsSection).SkipCount++ - continue - } - if opt.OptionSet() { - resp.Status(kolide.OptionsSection).Warning( - kolide.OptionAlreadySet, "skipped '%s' can't change option that is already set", optName, - ) - resp.Status(kolide.OptionsSection).SkipCount++ - continue - } - opt.SetValue(optValue) - resp.Status(kolide.OptionsSection).Message("set %s value to %v", optName, optValue) - resp.Status(kolide.OptionsSection).ImportCount++ - updateOptions = append(updateOptions, *opt) - } - if len(updateOptions) > 0 { - if err := svc.ds.SaveOptions(updateOptions, kolide.HasTransaction(tx)); err != nil { - return err - } - } - return nil -} - -func configInt2Ptr(ci *kolide.OsQueryConfigInt) *uint { - if ci == nil { - return nil - } - ui := uint(*ci) - return &ui -} diff --git a/server/service/service_import_config_test.go b/server/service/service_import_config_test.go deleted file mode 100644 index 3ee2b23bf..000000000 --- a/server/service/service_import_config_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package service - -import ( - "testing" - - "github.com/kolide/fleet/server/config" - "github.com/kolide/fleet/server/datastore/inmem" - "github.com/kolide/fleet/server/kolide" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func createServiceMockForImport(t *testing.T) *service { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - err = ds.MigrateData() - require.Nil(t, err) - return &service{ - ds: ds, - } -} - -func TestHashQuery(t *testing.T) { - q1 := `SELECT * FROM t1 INNER JOIN ON - t1.id = t2.t1id - WHERE t1.name = 'foo' ` - q2 := "SELECT * from t1 INNER JOIN\tON t1.id = t2.t1id WHERE t1.name = 'foo';" - h1 := hashQuery("platform", q1) - h2 := hashQuery("platform", q2) - assert.Equal(t, h1, h2) - q2 = "SELECT * from t1 INNER JOIN\tON t1.id = t2.t1id WHERE t2.name = 'foo';" - h2 = hashQuery("platform", q2) - assert.NotEqual(t, h1, h2) - -} -func TestImportFilePaths(t *testing.T) { - cfg := &kolide.ImportConfig{ - FileIntegrityMonitoring: kolide.FIMCategoryToPaths{ - "files1": []string{ - "path1", - "path2", - }, - "files2": []string{ - "path3", - }, - }, - YARA: &kolide.YARAConfig{ - Signatures: map[string][]string{ - "sig1": []string{ - "path4", - "path5", - }, - "sig2": []string{ - "path6", - }, - }, - FilePaths: map[string][]string{ - "files1": []string{ - "sig1", - "sig2", - }, - "files2": []string{ - "sig1", - }, - }, - }, - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - svc := createServiceMockForImport(t) - tx, _ := svc.ds.Begin() - err := svc.importFIMSections(cfg, resp, tx) - require.Nil(t, err) - assert.Equal(t, 2, resp.Status(kolide.FilePathsSection).ImportCount) - sections, err := svc.ds.FIMSections() - require.Nil(t, err) - assert.Len(t, sections, 2) - yara, err := svc.ds.YARASection() - require.Nil(t, err) - assert.Len(t, yara.Signatures, 2) - assert.Len(t, yara.FilePaths, 2) -} - -func TestImportDecorators(t *testing.T) { - cfg := &kolide.ImportConfig{ - Decorators: &kolide.DecoratorConfig{ - Load: []string{ - "select from foo", - "select from bar", - }, - Always: []string{ - "select from always", - }, - Interval: map[string][]string{ - "100": []string{ - "select from 100", - }, - "200": []string{ - "select from 200", - }, - }, - }, - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - svc := createServiceMockForImport(t) - tx, _ := svc.ds.Begin() - err := svc.importDecorators(cfg, resp, tx) - require.Nil(t, err) - assert.Equal(t, 5, resp.Status(kolide.DecoratorsSection).ImportCount) - dec, err := svc.ds.ListDecorators() - require.Nil(t, err) - assert.Len(t, dec, 5) -} - -func TestImportScheduledQueries(t *testing.T) { - cfg := &kolide.ImportConfig{ - Schedule: kolide.QueryNameToQueryDetailsMap{ - "q1": kolide.QueryDetails{ - Query: "select pid from processes", - Interval: 60, - Platform: stringPtr("linux"), - }, - "q2": kolide.QueryDetails{ - Query: "select uid from users", - Interval: 120, - Platform: stringPtr("linux"), - Version: stringPtr("1.0"), - }, - "q3": kolide.QueryDetails{ - Query: "select name from os", - Interval: 240, - Platform: stringPtr("linux"), - Snapshot: boolPtr(true), - }, - }, - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - svc := createServiceMockForImport(t) - user := &kolide.User{ - Username: "bob", - Password: []byte("secret"), - Email: "bob@something.com", - Admin: false, - AdminForcedPasswordReset: false, - } - user, err := svc.ds.NewUser(user) - require.Nil(t, err) - skipQuery := &kolide.Query{ - Name: "q3", - Query: "select version from os", - Description: "should be skipped", - Saved: true, - AuthorID: user.ID, - } - _, err = svc.ds.NewQuery(skipQuery) - require.Nil(t, err) - noskipQuery := &kolide.Query{ - Name: "q2", - Query: "select uid from users", - Saved: true, - AuthorID: user.ID, - } - _, err = svc.ds.NewQuery(noskipQuery) - require.Nil(t, err) - tx, _ := svc.ds.Begin() - err = svc.importScheduledQueries(user.ID, cfg, resp, tx) - require.Nil(t, err) - _, ok, err := svc.ds.QueryByName("q1") - require.Nil(t, err) - require.True(t, ok) - _, ok, err = svc.ds.QueryByName("q2") - require.Nil(t, err) - require.True(t, ok) - _, ok, err = svc.ds.QueryByName("q3") - require.Nil(t, err) - require.True(t, ok) - -} - -func TestOptionsImportConfig(t *testing.T) { - opts := kolide.OptionNameToValueMap{ - "aws_access_key_id": "foo", - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - svc := createServiceMockForImport(t) - tx, _ := svc.ds.Begin() - err := svc.importOptions(opts, resp, tx) - require.Nil(t, err) - status := resp.Status(kolide.OptionsSection) - require.NotNil(t, status) - assert.Equal(t, 1, status.ImportCount) - opt, err := svc.ds.OptionByName("aws_access_key_id") - require.Nil(t, err) - assert.Equal(t, "foo", opt.GetValue()) - require.Len(t, status.Messages, 1) - assert.Equal(t, "set aws_access_key_id value to foo", status.Messages[0]) -} - -func TestOptionsImportConfigWithSkips(t *testing.T) { - opts := kolide.OptionNameToValueMap{ - "aws_access_key_id": "foo", - "aws_secret_access_key": "secret", - // this should be skipped because it's already set - "aws_firehose_period": 100, - // these should be skipped because it's read only - "disable_distributed": false, - "pack_delimiter": "x", - // this should be skipped because it's not an option we know about - "wombat": "not venomous", - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - svc := createServiceMockForImport(t) - tx, _ := svc.ds.Begin() - // set option val, it should be skipped - opt, err := svc.ds.OptionByName("aws_firehose_period") - require.Nil(t, err) - opt.SetValue(23) - err = svc.ds.SaveOptions([]kolide.Option{*opt}) - require.Nil(t, err) - err = svc.importOptions(opts, resp, tx) - require.Nil(t, err) - status := resp.Status(kolide.OptionsSection) - require.NotNil(t, status) - assert.Equal(t, 2, status.ImportCount) - assert.Equal(t, 4, status.SkipCount) - assert.Len(t, status.Warnings[kolide.OptionAlreadySet], 1) - assert.Len(t, status.Warnings[kolide.OptionReadonly], 2) - assert.Len(t, status.Warnings[kolide.OptionUnknown], 1) - assert.Len(t, status.Messages, 2) -} - -func TestPacksImportConfig(t *testing.T) { - svc := createServiceMockForImport(t) - tx, _ := svc.ds.Begin() - - p := &kolide.Pack{ - Name: "dup", - } - _, err := svc.ds.NewPack(p) - require.Nil(t, err) - - q1 := kolide.QueryDetails{ - Query: "select * from foo", - Interval: 100, - Removed: boolPtr(false), - Platform: stringPtr("linux"), - Version: stringPtr("1.0"), - } - q2 := kolide.QueryDetails{ - Query: "select * from bar", - Interval: 50, - Removed: boolPtr(false), - Platform: stringPtr("linux"), - Version: stringPtr("1.0"), - } - q3 := kolide.QueryDetails{ - Query: "select * from baz", - Interval: 500, - Removed: boolPtr(false), - Platform: stringPtr("linux"), - Version: stringPtr("1.0"), - } - - importConfig := kolide.ImportConfig{ - Packs: kolide.PackNameMap{ - "ext1": "/home/usr/ext1.json", - "pack1": kolide.PackDetails{ - Queries: kolide.QueryNameToQueryDetailsMap{ - "q1": q1, - "q2": q2, - }, - Discovery: []string{ - "select * from zz", - "select id, xx from yy", - }, - }, - "dup": kolide.PackDetails{ - Queries: kolide.QueryNameToQueryDetailsMap{ - "q1": q1, - "q2": q2, - }, - }, - "*": "/home/usr/packs/*", - }, - ExternalPacks: kolide.PackNameToPackDetails{ - "ext1": kolide.PackDetails{ - Queries: kolide.QueryNameToQueryDetailsMap{ - "q1": q1, - }, - Discovery: []string{ - "select * from zz", - "select a, b, c from processes", - }, - }, - "ext2": kolide.PackDetails{ - Queries: kolide.QueryNameToQueryDetailsMap{ - "q3": q3, - }, - }, - }, - GlobPackNames: []string{"ext2"}, - } - resp := &kolide.ImportConfigResponse{ - ImportStatusBySection: make(map[kolide.ImportSection]*kolide.ImportStatus), - } - user := &kolide.User{ - Username: "bob", - Password: []byte("secret"), - Email: "bob@something.com", - Admin: false, - AdminForcedPasswordReset: false, - } - user, err = svc.ds.NewUser(user) - require.Nil(t, err) - - packs, err := importConfig.CollectPacks() - require.Nil(t, err) - assert.Len(t, packs, 4) - err = svc.importPacks(user.ID, &importConfig, resp, tx) - require.Nil(t, err) - queries, err := svc.ds.ListQueries(kolide.ListOptions{}) - require.Nil(t, err) - assert.Len(t, queries, 3) - pack, ok, err := svc.ds.PackByName("pack1") - require.Nil(t, err) - require.True(t, ok) - sqs, err := svc.ds.ListScheduledQueriesInPack(pack.ID, kolide.ListOptions{}) - require.Nil(t, err) - assert.Len(t, sqs, 2) - labels, err := svc.ds.ListLabels(kolide.ListOptions{}) - require.Nil(t, err) - assert.Len(t, labels, 8) - assert.Equal(t, 3, resp.Status(kolide.PacksSection).ImportCount) - assert.Equal(t, 1, resp.Status(kolide.PacksSection).SkipCount) - assert.Equal(t, 3, resp.Status(kolide.QueriesSection).ImportCount) -} diff --git a/server/service/service_labels.go b/server/service/service_labels.go index 85c0cd3b7..49c2d4495 100644 --- a/server/service/service_labels.go +++ b/server/service/service_labels.go @@ -6,6 +6,18 @@ import ( "github.com/kolide/fleet/server/kolide" ) +func (svc service) ApplyLabelSpecs(ctx context.Context, specs []*kolide.LabelSpec) error { + return svc.ds.ApplyLabelSpecs(specs) +} + +func (svc service) GetLabelSpecs(ctx context.Context) ([]*kolide.LabelSpec, error) { + return svc.ds.GetLabelSpecs() +} + +func (svc service) GetLabelSpec(ctx context.Context, name string) (*kolide.LabelSpec, error) { + return svc.ds.GetLabelSpec(name) +} + func (svc service) ListLabels(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Label, error) { return svc.ds.ListLabels(opt) } @@ -14,36 +26,8 @@ func (svc service) GetLabel(ctx context.Context, id uint) (*kolide.Label, error) return svc.ds.Label(id) } -func (svc service) NewLabel(ctx context.Context, p kolide.LabelPayload) (*kolide.Label, error) { - label := &kolide.Label{} - - if p.Name == nil { - return nil, newInvalidArgumentError("name", "missing required argument") - } - label.Name = *p.Name - - if p.Query == nil { - return nil, newInvalidArgumentError("query", "missing required argument") - } - label.Query = *p.Query - - if p.Platform != nil { - label.Platform = *p.Platform - } - - if p.Description != nil { - label.Description = *p.Description - } - - label, err := svc.ds.NewLabel(label) - if err != nil { - return nil, err - } - return label, nil -} - -func (svc service) DeleteLabel(ctx context.Context, id uint) error { - return svc.ds.DeleteLabel(id) +func (svc service) DeleteLabel(ctx context.Context, name string) error { + return svc.ds.DeleteLabel(name) } func (svc service) HostIDsForLabel(lid uint) ([]uint, error) { @@ -57,17 +41,3 @@ func (svc service) HostIDsForLabel(lid uint) ([]uint, error) { } return ids, nil } - -func (svc service) ModifyLabel(ctx context.Context, id uint, payload kolide.ModifyLabelPayload) (*kolide.Label, error) { - label, err := svc.ds.Label(id) - if err != nil { - return nil, err - } - if payload.Name != nil { - label.Name = *payload.Name - } - if payload.Description != nil { - label.Description = *payload.Description - } - return svc.ds.SaveLabel(label) -} diff --git a/server/service/service_labels_test.go b/server/service/service_labels_test.go index b99d4d2f7..d9da3a822 100644 --- a/server/service/service_labels_test.go +++ b/server/service/service_labels_test.go @@ -7,62 +7,9 @@ import ( "github.com/kolide/fleet/server/config" "github.com/kolide/fleet/server/datastore/inmem" "github.com/kolide/fleet/server/kolide" - "github.com/kolide/fleet/server/mock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestModifyLabel(t *testing.T) { - ds := new(mock.Store) - ds.LabelFunc = func(id uint) (*kolide.Label, error) { - l := &kolide.Label{ - ID: id, - Name: "name", - Description: "desc", - } - return l, nil - } - ds.SaveLabelFunc = func(l *kolide.Label) (*kolide.Label, error) { - return l, nil - } - svc, err := newTestService(ds, nil) - require.Nil(t, err) - lp := kolide.ModifyLabelPayload{ - Name: stringPtr("new name"), - Description: stringPtr("new desc"), - } - l, err := svc.ModifyLabel(context.Background(), uint(1), lp) - assert.Equal(t, "new name", l.Name) - assert.Equal(t, "new desc", l.Description) - assert.True(t, ds.LabelFuncInvoked) - assert.True(t, ds.SaveLabelFuncInvoked) -} - -func TestListLabels(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - labels, err := svc.ListLabels(ctx, kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, labels, 0) - - _, err = ds.NewLabel(&kolide.Label{ - Name: "foo", - Query: "select * from foo;", - }) - assert.Nil(t, err) - - labels, err = svc.ListLabels(ctx, kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, labels, 1) - assert.Equal(t, "foo", labels[0].Name) -} - func TestGetLabel(t *testing.T) { ds, err := inmem.New(config.TestConfig()) assert.Nil(t, err) @@ -84,53 +31,3 @@ func TestGetLabel(t *testing.T) { assert.Nil(t, err) assert.Equal(t, label.ID, labelVerify.ID) } - -func TestNewLabel(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - name := "foo" - query := "select * from foo;" - label, err := svc.NewLabel(ctx, kolide.LabelPayload{ - Name: &name, - Query: &query, - }) - assert.NotZero(t, label.ID) - - assert.Nil(t, err) - - labels, err := ds.ListLabels(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, labels, 1) - assert.Equal(t, "foo", labels[0].Name) -} - -func TestDeleteLabel(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - label := &kolide.Label{ - Name: "foo", - Query: "select * from foo;", - } - label, err = ds.NewLabel(label) - assert.Nil(t, err) - assert.NotZero(t, label.ID) - - err = svc.DeleteLabel(ctx, label.ID) - assert.Nil(t, err) - - labels, err := ds.ListLabels(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, labels, 0) -} diff --git a/server/service/service_osquery.go b/server/service/service_osquery.go index 8ec8bd02d..d8cf5c41e 100644 --- a/server/service/service_osquery.go +++ b/server/service/service_osquery.go @@ -89,69 +89,29 @@ func (svc service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier return host.NodeKey, nil } -func (svc service) getFIMConfig(ctx context.Context, cfg *kolide.OsqueryConfig) (*kolide.OsqueryConfig, error) { - fimConfig, err := svc.GetFIM(ctx) - if err != nil { - return nil, osqueryError{message: "internal error: unable to fetch FIM configuration"} - } - if cfg.Schedule == nil { - cfg.Schedule = make(map[string]kolide.QueryContent) - } - removed := false - // file events scheduled query is required to run file integrity monitors - cfg.Schedule["file_events"] = kolide.QueryContent{ - Query: "SELECT * FROM file_events;", - Interval: fimConfig.Interval, - Removed: &removed, - } - cfg.FilePaths = fimConfig.FilePaths - return cfg, nil -} - -func (svc service) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, error) { +func (svc service) GetClientConfig(ctx context.Context) (map[string]interface{}, error) { host, ok := hostctx.FromContext(ctx) if !ok { return nil, osqueryError{message: "internal error: missing host from request context"} } - options, err := svc.ds.GetOsqueryConfigOptions() + baseConfig, err := svc.ds.OptionsForPlatform(host.Platform) if err != nil { - return nil, osqueryError{message: "internal error: unable to fetch configuration options"} + return nil, osqueryError{message: "internal error: fetching base config: " + err.Error()} } - decorators, err := svc.ds.ListDecorators() + var config map[string]interface{} + err = json.Unmarshal(baseConfig, &config) if err != nil { - return nil, osqueryError{message: "internal error: unable to fetch decorators"} - } - decConfig := kolide.Decorators{ - Interval: make(map[string][]string), - } - for _, dec := range decorators { - switch dec.Type { - case kolide.DecoratorLoad: - decConfig.Load = append(decConfig.Load, dec.Query) - case kolide.DecoratorAlways: - decConfig.Always = append(decConfig.Always, dec.Query) - case kolide.DecoratorInterval: - key := strconv.Itoa(int(dec.Interval)) - decConfig.Interval[key] = append(decConfig.Interval[key], dec.Query) - default: - svc.logger.Log("component", "service", "method", "GetClientConfig", "err", - "unknown decorator type") - } + return nil, osqueryError{message: "internal error: parsing base configuration: " + err.Error()} } - config := &kolide.OsqueryConfig{ - Options: options, - Decorators: decConfig, - Packs: kolide.Packs{}, - } - - packs, err := svc.ListPacksForHost(ctx, host.ID) + packs, err := svc.ds.ListPacksForHost(host.ID) if err != nil { return nil, osqueryError{message: "database error: " + err.Error()} } + packConfig := kolide.Packs{} for _, pack := range packs { // first, we must figure out what queries are in this pack queries, err := svc.ds.ListScheduledQueriesInPack(pack.ID, kolide.ListOptions{}) @@ -184,30 +144,40 @@ func (svc service) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, } // finally, we add the pack to the client config struct with all of - // the packs queries - config.Packs[pack.Name] = kolide.PackContent{ + // the pack's queries + packConfig[pack.Name] = kolide.PackContent{ Platform: pack.Platform, Queries: configQueries, } } + if len(packConfig) > 0 { + packJSON, err := json.Marshal(packConfig) + if err != nil { + return nil, osqueryError{message: "internal error: marshal pack JSON: " + err.Error()} + } + config["packs"] = json.RawMessage(packJSON) + } + // Save interval values if they have been updated. Note // config_tls_refresh can only be set in the osquery flags so is // ignored here. saveHost := false - distributedIntervalVal, ok := config.Options["distributed_interval"] - distributedInterval, err := cast.ToUintE(distributedIntervalVal) - if ok && err == nil && host.DistributedInterval != distributedInterval { - host.DistributedInterval = distributedInterval - saveHost = true - } + if options, ok := config["options"].(map[string]interface{}); ok { + distributedIntervalVal, ok := options["distributed_interval"] + distributedInterval, err := cast.ToUintE(distributedIntervalVal) + if ok && err == nil && host.DistributedInterval != distributedInterval { + host.DistributedInterval = distributedInterval + saveHost = true + } - loggerTLSPeriodVal, ok := config.Options["logger_tls_period"] - loggerTLSPeriod, err := cast.ToUintE(loggerTLSPeriodVal) - if ok && err == nil && host.LoggerTLSPeriod != loggerTLSPeriod { - host.LoggerTLSPeriod = loggerTLSPeriod - saveHost = true + loggerTLSPeriodVal, ok := options["logger_tls_period"] + loggerTLSPeriod, err := cast.ToUintE(loggerTLSPeriodVal) + if ok && err == nil && host.LoggerTLSPeriod != loggerTLSPeriod { + host.LoggerTLSPeriod = loggerTLSPeriod + saveHost = true + } } if saveHost { @@ -217,7 +187,7 @@ func (svc service) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, } } - return svc.getFIMConfig(ctx, config) + return config, nil } // If osqueryWriters are based on bufio we want to flush after a batch of diff --git a/server/service/service_osquery_options.go b/server/service/service_osquery_options.go new file mode 100644 index 000000000..e198c0fd4 --- /dev/null +++ b/server/service/service_osquery_options.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + + "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" +) + +func (svc service) ApplyOptionsSpec(ctx context.Context, spec *kolide.OptionsSpec) error { + err := svc.ds.ApplyOptions(spec) + if err != nil { + return errors.Wrap(err, "apply options") + } + return nil +} + +func (svc service) GetOptionsSpec(ctx context.Context) (*kolide.OptionsSpec, error) { + spec, err := svc.ds.GetOptions() + if err != nil { + return nil, errors.Wrap(err, "get options from datastore") + } + + return spec, nil +} diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index 635ec266b..a260f2b4f 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -20,7 +20,6 @@ import ( "github.com/kolide/fleet/server/kolide" "github.com/kolide/fleet/server/mock" "github.com/kolide/fleet/server/pubsub" - "github.com/kolide/fleet/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -220,19 +219,33 @@ func TestHostDetailQueries(t *testing.T) { } } +func TestGetDistributedQueriesMissingHost(t *testing.T) { + svc, err := newTestService(&mock.Store{}, nil) + require.Nil(t, err) + + _, _, err = svc.GetDistributedQueries(context.Background()) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "missing host") +} + func TestLabelQueries(t *testing.T) { - ds, svc, mockClock := setupOsqueryTests(t) - ctx := context.Background() - - _, err := svc.EnrollAgent(ctx, "", "host123") + mockClock := clock.NewMockClock() + ds := new(mock.Store) + svc, err := newTestServiceWithClock(ds, nil, mockClock) require.Nil(t, err) - hosts, err := ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 1) - host := hosts[0] + ds.LabelQueriesForHostFunc = func(host *kolide.Host, cutoff time.Time) (map[string]string, error) { + return map[string]string{}, nil + } + ds.DistributedQueriesForHostFunc = func(host *kolide.Host) (map[uint]string, error) { + return map[uint]string{}, nil + } + ds.SaveHostFunc = func(host *kolide.Host) error { + return nil + } - ctx = hostctx.NewContext(ctx, *host) + host := &kolide.Host{} + ctx := hostctx.NewContext(context.Background(), *host) // With a new host, we should get the detail queries (and accelerate // should be turned on so that we can quickly fill labels) @@ -245,7 +258,6 @@ func TestLabelQueries(t *testing.T) { host.DetailUpdateTime = mockClock.Now().Add(-1 * time.Minute) host.Platform = "darwin" host.HostName = "zwass.local" - ds.SaveHost(host) ctx = hostctx.NewContext(ctx, *host) queries, acc, err = svc.GetDistributedQueries(ctx) @@ -253,32 +265,12 @@ func TestLabelQueries(t *testing.T) { assert.Len(t, queries, 0) assert.Zero(t, acc) - labels := []kolide.Label{ - kolide.Label{ - Name: "label1", - Query: "query1", - Platform: "darwin", - }, - kolide.Label{ - Name: "label2", - Query: "query2", - Platform: "darwin", - }, - kolide.Label{ - Name: "label3", - Query: "query3", - Platform: "darwin,linux", - }, - kolide.Label{ - Name: "label4", - Query: "query4", - Platform: "linux", - }, - } - - for _, label := range labels { - _, err := ds.NewLabel(&label) - assert.Nil(t, err) + ds.LabelQueriesForHostFunc = func(host *kolide.Host, cutoff time.Time) (map[string]string, error) { + return map[string]string{ + "label1": "query1", + "label2": "query2", + "label3": "query3", + }, nil } // Now we should get the label queries @@ -287,6 +279,16 @@ func TestLabelQueries(t *testing.T) { assert.Len(t, queries, 3) assert.Zero(t, acc) + var gotHost *kolide.Host + var gotResults map[uint]bool + var gotTime time.Time + ds.RecordLabelQueryExecutionsFunc = func(host *kolide.Host, results map[uint]bool, t time.Time) error { + gotHost = host + gotResults = results + gotTime = t + return nil + } + // Record a query execution err = svc.SubmitDistributedQueryResults( ctx, @@ -296,32 +298,13 @@ func TestLabelQueries(t *testing.T) { map[string]kolide.OsqueryStatus{}, ) assert.Nil(t, err) + assert.Equal(t, host, gotHost) + assert.Equal(t, mockClock.Now(), gotTime) + if assert.Len(t, gotResults, 1) { + assert.Equal(t, true, gotResults[1]) + } - // Verify that labels are set appropriately - hostLabels, err := ds.ListLabelsForHost(host.ID) - assert.Len(t, hostLabels, 1) - assert.Equal(t, "label1", hostLabels[0].Name) - - // Now that query should not be returned - queries, acc, err = svc.GetDistributedQueries(ctx) - assert.Nil(t, err) - assert.Len(t, queries, 2) - assert.NotContains(t, queries, "kolide_label_query_1") - assert.Zero(t, acc) - - // Advance the time - mockClock.AddTime(1*time.Hour + 1*time.Minute) - - // Keep the host details fresh - host.DetailUpdateTime = mockClock.Now().Add(-1 * time.Minute) - ds.SaveHost(host) - ctx = hostctx.NewContext(ctx, *host) - - // Now we should get all the label queries again - queries, acc, err = svc.GetDistributedQueries(ctx) - assert.Nil(t, err) - assert.Len(t, queries, 3) - assert.Zero(t, acc) + mockClock.AddTime(1 * time.Second) // Record a query execution err = svc.SubmitDistributedQueryResults( @@ -333,110 +316,153 @@ func TestLabelQueries(t *testing.T) { map[string]kolide.OsqueryStatus{}, ) assert.Nil(t, err) - - // Now these should no longer show up in the necessary to run queries - queries, acc, err = svc.GetDistributedQueries(ctx) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.Zero(t, acc) - - // Verify that labels are set appropriately - hostLabels, err = ds.ListLabelsForHost(host.ID) - assert.Len(t, hostLabels, 2) - expectLabelNames := map[string]bool{"label1": true, "label2": true} - for _, label := range hostLabels { - assert.Contains(t, expectLabelNames, label.Name) - delete(expectLabelNames, label.Name) + assert.Equal(t, host, gotHost) + assert.Equal(t, mockClock.Now(), gotTime) + if assert.Len(t, gotResults, 2) { + assert.Equal(t, true, gotResults[2]) + assert.Equal(t, false, gotResults[3]) } } func TestGetClientConfig(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - require.Nil(t, ds.MigrateData()) - - mockClock := clock.NewMockClock() - - svc, err := newTestServiceWithClock(ds, nil, mockClock) - assert.Nil(t, err) - - ctx := context.Background() - - hosts, err := ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 0) - - _, err = svc.EnrollAgent(ctx, "", "user.local") - assert.Nil(t, err) - - hosts, err = ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 1) - host := hosts[0] - - ctx = hostctx.NewContext(ctx, *host) - - // with no queries, packs, labels, etc. verify the state of a fresh host - // asking for a config - config, err := svc.GetClientConfig(ctx) - require.Nil(t, err) - assert.NotNil(t, config) - val, ok := config.Options["disable_distributed"] - require.True(t, ok) - disabled, ok := val.(bool) - require.True(t, ok) - assert.False(t, disabled) - val, ok = config.Options["pack_delimiter"] - require.True(t, ok) - delim, ok := val.(string) - require.True(t, ok) - assert.Equal(t, "/", delim) - - // this will be greater than 0 if we ever start inserting an administration - // pack - assert.Len(t, config.Packs, 0) - - // let's populate the database with some info - - infoQuery := &kolide.Query{ - Name: "Info", - Query: "select * from osquery_info;", + ds := new(mock.Store) + ds.ListPacksForHostFunc = func(hid uint) ([]*kolide.Pack, error) { + return []*kolide.Pack{}, nil } - infoQueryInterval := uint(60) - infoQuery, err = ds.NewQuery(infoQuery) - assert.Nil(t, err) - - monitoringPack := &kolide.Pack{ - Name: "monitoring", + ds.ListScheduledQueriesInPackFunc = func(pid uint, opt kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { + tru := true + fals := false + fortytwo := uint(42) + switch pid { + case 1: + return []*kolide.ScheduledQuery{ + {Name: "time", Query: "select * from time", Interval: 30, Removed: &fals}, + }, nil + case 4: + return []*kolide.ScheduledQuery{ + {Name: "foobar", Query: "select 3", Interval: 20, Shard: &fortytwo}, + {Name: "froobing", Query: "select 'guacamole'", Interval: 60, Snapshot: &tru}, + }, nil + default: + return []*kolide.ScheduledQuery{}, nil + } } - _, err = ds.NewPack(monitoringPack) - assert.Nil(t, err) - - test.NewScheduledQuery(t, ds, monitoringPack.ID, infoQuery.ID, infoQueryInterval, false, false) - - mysqlLabel := &kolide.Label{ - Name: "MySQL Monitoring", - Query: "select pid from processes where name = 'mysqld';", + ds.OptionsForPlatformFunc = func(platform string) (json.RawMessage, error) { + return json.RawMessage(` +{ + "options":{ + "distributed_interval":11, + "logger_tls_period":33 + }, + "decorators":{ + "load":[ + "SELECT version FROM osquery_info;", + "SELECT uuid AS host_uuid FROM system_info;" + ], + "always":[ + "SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;" + ], + "interval":{ + "3600":[ + "SELECT total_seconds AS uptime FROM uptime;" + ] + } + }, + "foo": "bar" +} +`), nil + } + ds.SaveHostFunc = func(host *kolide.Host) error { + return nil } - mysqlLabel, err = ds.NewLabel(mysqlLabel) - assert.Nil(t, err) - err = ds.AddLabelToPack(mysqlLabel.ID, monitoringPack.ID) - assert.Nil(t, err) + svc, err := newTestService(ds, nil) + require.Nil(t, err) - err = ds.RecordLabelQueryExecutions( - host, - map[uint]bool{mysqlLabel.ID: true}, - mockClock.Now(), + ctx1 := hostctx.NewContext(context.Background(), kolide.Host{ID: 1}) + ctx2 := hostctx.NewContext(context.Background(), kolide.Host{ID: 2}) + + expectedOptions := map[string]interface{}{ + "distributed_interval": float64(11), + "logger_tls_period": float64(33), + } + + expectedDecorators := map[string]interface{}{ + "load": []interface{}{ + "SELECT version FROM osquery_info;", + "SELECT uuid AS host_uuid FROM system_info;", + }, + "always": []interface{}{ + "SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;", + }, + "interval": map[string]interface{}{ + "3600": []interface{}{"SELECT total_seconds AS uptime FROM uptime;"}, + }, + } + + expectedConfig := map[string]interface{}{ + "options": expectedOptions, + "decorators": expectedDecorators, + "foo": "bar", + } + + // No packs loaded yet + conf, err := svc.GetClientConfig(ctx1) + require.Nil(t, err) + assert.Equal(t, expectedConfig, conf) + + conf, err = svc.GetClientConfig(ctx2) + require.Nil(t, err) + assert.Equal(t, expectedConfig, conf) + + // Now add packs + ds.ListPacksForHostFunc = func(hid uint) ([]*kolide.Pack, error) { + switch hid { + case 1: + return []*kolide.Pack{ + {ID: 1, Name: "pack_by_label"}, + {ID: 4, Name: "pack_by_other_label"}, + }, nil + + case 2: + return []*kolide.Pack{ + {ID: 1, Name: "pack_by_label"}, + }, nil + } + return []*kolide.Pack{}, nil + } + + conf, err = svc.GetClientConfig(ctx1) + require.Nil(t, err) + assert.Equal(t, expectedOptions, conf["options"]) + assert.JSONEq(t, `{ + "pack_by_other_label": { + "queries": { + "foobar":{"query":"select 3","interval":20,"shard":42}, + "froobing":{"query":"select 'guacamole'","interval":60,"snapshot":true} + } + }, + "pack_by_label": { + "queries":{ + "time":{"query":"select * from time","interval":30,"removed":false} + } + } + }`, + string(conf["packs"].(json.RawMessage)), ) - assert.Nil(t, err) - // with a minimal setup of packs, labels, and queries, will our host get the - // pack - config, err = svc.GetClientConfig(ctx) + conf, err = svc.GetClientConfig(ctx2) require.Nil(t, err) - assert.Len(t, config.Packs, 1) - assert.Len(t, config.Packs["monitoring"].Queries, 1) + assert.Equal(t, expectedOptions, conf["options"]) + assert.JSONEq(t, `{ + "pack_by_label": { + "queries":{ + "time":{"query":"select * from time","interval":30,"removed":false} + } + } + }`, + string(conf["packs"].(json.RawMessage)), + ) } func TestDetailQueriesWithEmptyStrings(t *testing.T) { @@ -771,71 +797,97 @@ func TestDetailQueries(t *testing.T) { assert.Zero(t, acc) } -func TestDistributedQueries(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - - _, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: ""}) - require.Nil(t, err) - +func TestNewDistributedQueryCampaign(t *testing.T) { mockClock := clock.NewMockClock() - - rs := pubsub.NewInmemQueryResults() - - svc, err := newTestServiceWithClock(ds, rs, mockClock) + ds := new(mock.Store) + svc, err := newTestServiceWithClock(ds, nil, mockClock) require.Nil(t, err) - ctx := context.Background() + ds.LabelQueriesForHostFunc = func(host *kolide.Host, cutoff time.Time) (map[string]string, error) { + return map[string]string{}, nil + } + ds.DistributedQueriesForHostFunc = func(host *kolide.Host) (map[uint]string, error) { + return map[uint]string{}, nil + } + ds.SaveHostFunc = func(host *kolide.Host) error { + return nil + } + var gotQuery *kolide.Query + ds.NewQueryFunc = func(query *kolide.Query, opts ...kolide.OptionalArg) (*kolide.Query, error) { + gotQuery = query + query.ID = 42 + return query, nil + } + var gotCampaign *kolide.DistributedQueryCampaign + ds.NewDistributedQueryCampaignFunc = func(camp *kolide.DistributedQueryCampaign) (*kolide.DistributedQueryCampaign, error) { + gotCampaign = camp + camp.ID = 21 + return camp, nil + } + var gotTargets []*kolide.DistributedQueryCampaignTarget + ds.NewDistributedQueryCampaignTargetFunc = func(target *kolide.DistributedQueryCampaignTarget) (*kolide.DistributedQueryCampaignTarget, error) { + gotTargets = append(gotTargets, target) + return target, nil + } - nodeKey, err := svc.EnrollAgent(ctx, "", "host123") - require.Nil(t, err) - - host, err := ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - - host.Platform = "centos" - host.HostName = "zwass.local" - require.Nil(t, ds.SaveHost(host)) - - // Create label - n := "foo" - q := "select * from foo;" - label, err := svc.NewLabel(ctx, kolide.LabelPayload{ - Name: &n, - Query: &q, - }) - require.Nil(t, err) - - // Record match with label - ctx = viewer.NewContext(ctx, viewer.Viewer{ + viewerCtx := viewer.NewContext(context.Background(), viewer.Viewer{ User: &kolide.User{ ID: 0, }, }) - err = ds.RecordLabelQueryExecutions(host, map[uint]bool{label.ID: true}, mockClock.Now()) + q := "select year, month, day, hour, minutes, seconds from time" + campaign, err := svc.NewDistributedQueryCampaign(viewerCtx, q, []uint{2}, []uint{1}) require.Nil(t, err) - err = ds.MarkHostSeen(host, mockClock.Now()) - require.Nil(t, err) - ctx = hostctx.NewContext(ctx, *host) + assert.Equal(t, gotQuery.ID, gotCampaign.QueryID) + assert.Equal(t, []*kolide.DistributedQueryCampaignTarget{ + &kolide.DistributedQueryCampaignTarget{ + Type: kolide.TargetHost, + DistributedQueryCampaignID: campaign.ID, + TargetID: 2, + }, + &kolide.DistributedQueryCampaignTarget{ + Type: kolide.TargetLabel, + DistributedQueryCampaignID: campaign.ID, + TargetID: 1, + }, + }, gotTargets, + ) +} - q = "select year, month, day, hour, minutes, seconds from time" - campaign, err := svc.NewDistributedQueryCampaign(ctx, q, []uint{}, []uint{label.ID}) +func TestDistributedQueryResults(t *testing.T) { + mockClock := clock.NewMockClock() + ds := new(mock.Store) + rs := pubsub.NewInmemQueryResults() + svc, err := newTestServiceWithClock(ds, rs, mockClock) require.Nil(t, err) - // Manually set the campaign to running (so that it shows up when - // requesting queries) - campaign.Status = kolide.QueryRunning - err = ds.SaveDistributedQueryCampaign(campaign) - require.Nil(t, err) + campaign := &kolide.DistributedQueryCampaign{ID: 42} - queryKey := fmt.Sprintf("%s%d", hostDistributedQueryPrefix, campaign.ID) + ds.LabelQueriesForHostFunc = func(host *kolide.Host, cutoff time.Time) (map[string]string, error) { + return map[string]string{}, nil + } + ds.SaveHostFunc = func(host *kolide.Host) error { + return nil + } + ds.DistributedQueriesForHostFunc = func(host *kolide.Host) (map[uint]string, error) { + return map[uint]string{campaign.ID: "select * from time"}, nil + } + var gotExecution *kolide.DistributedQueryExecution + ds.NewDistributedQueryExecutionFunc = func(exec *kolide.DistributedQueryExecution) (*kolide.DistributedQueryExecution, error) { + gotExecution = exec + return exec, nil + } + + host := &kolide.Host{ID: 1} + hostCtx := hostctx.NewContext(context.Background(), *host) // Now we should get the active distributed query - queries, acc, err := svc.GetDistributedQueries(ctx) + queries, acc, err := svc.GetDistributedQueries(hostCtx) require.Nil(t, err) assert.Len(t, queries, len(detailQueries)+1) - assert.Equal(t, q, queries[queryKey]) - assert.Zero(t, acc) + queryKey := fmt.Sprintf("%s%d", hostDistributedQueryPrefix, campaign.ID) + assert.Equal(t, "select * from time", queries[queryKey]) + assert.NotZero(t, acc) expectedRows := []map[string]string{ { @@ -852,7 +904,7 @@ func TestDistributedQueries(t *testing.T) { } // TODO use service method - readChan, err := rs.ReadChannel(ctx, *campaign) + readChan, err := rs.ReadChannel(context.Background(), *campaign) require.Nil(t, err) // We need to listen for the result in a separate thread to prevent the @@ -887,17 +939,11 @@ func TestDistributedQueries(t *testing.T) { // this test. time.Sleep(10 * time.Millisecond) - err = svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) + err = svc.SubmitDistributedQueryResults(hostCtx, results, map[string]kolide.OsqueryStatus{}) require.Nil(t, err) - - // Now the distributed query should be completed and not returned - queries, acc, err = svc.GetDistributedQueries(ctx) - require.Nil(t, err) - assert.Len(t, queries, len(detailQueries)) - assert.NotContains(t, queries, queryKey) - assert.Zero(t, acc) - - waitComplete.Wait() + assert.Equal(t, campaign.ID, gotExecution.DistributedQueryCampaignID) + assert.Equal(t, host.ID, gotExecution.HostID) + assert.Equal(t, kolide.ExecutionSucceeded, gotExecution.Status) } func TestOrphanedQueryCampaign(t *testing.T) { @@ -966,31 +1012,14 @@ func TestUpdateHostIntervals(t *testing.T) { svc, err := newTestService(ds, nil) require.Nil(t, err) - ds.ListDecoratorsFunc = func(opt ...kolide.OptionalArg) ([]*kolide.Decorator, error) { - return []*kolide.Decorator{}, nil - } - ds.ListPacksFunc = func(opt kolide.ListOptions) ([]*kolide.Pack, error) { + ds.ListPacksForHostFunc = func(hid uint) ([]*kolide.Pack, error) { return []*kolide.Pack{}, nil } - ds.ListLabelsForHostFunc = func(hid uint) ([]kolide.Label, error) { - return []kolide.Label{}, nil - } - ds.AppConfigFunc = func() (*kolide.AppConfig, error) { - return &kolide.AppConfig{FIMInterval: 400}, nil - } - ds.FIMSectionsFunc = func() (kolide.FIMSections, error) { - sections := kolide.FIMSections{ - "etc": []string{ - "/etc/%%", - }, - } - return sections, nil - } var testCases = []struct { initHost kolide.Host finalHost kolide.Host - configOptions map[string]interface{} + configOptions json.RawMessage saveHostCalled bool }{ // Both updated @@ -1003,11 +1032,11 @@ func TestUpdateHostIntervals(t *testing.T) { LoggerTLSPeriod: 33, ConfigTLSRefresh: 60, }, - map[string]interface{}{ + json.RawMessage(`{"options": { "distributed_interval": 11, "logger_tls_period": 33, - "logger_plugin": "tls", - }, + "logger_plugin": "tls" + }}`), true, }, // Only logger_tls_period updated @@ -1021,10 +1050,10 @@ func TestUpdateHostIntervals(t *testing.T) { LoggerTLSPeriod: 33, ConfigTLSRefresh: 60, }, - map[string]interface{}{ + json.RawMessage(`{"options": { "distributed_interval": 11, - "logger_tls_period": 33, - }, + "logger_tls_period": 33 + }}`), true, }, // Only distributed_interval updated @@ -1038,10 +1067,10 @@ func TestUpdateHostIntervals(t *testing.T) { LoggerTLSPeriod: 33, ConfigTLSRefresh: 60, }, - map[string]interface{}{ + json.RawMessage(`{"options": { "distributed_interval": 11, - "logger_tls_period": 33, - }, + "logger_tls_period": 33 + }}`), true, }, // Kolide not managing distributed_interval @@ -1055,9 +1084,9 @@ func TestUpdateHostIntervals(t *testing.T) { LoggerTLSPeriod: 33, ConfigTLSRefresh: 60, }, - map[string]interface{}{ - "logger_tls_period": 33, - }, + json.RawMessage(`{"options":{ + "logger_tls_period": 33 + }}`), true, }, // SaveHost should not be called with no changes @@ -1072,21 +1101,19 @@ func TestUpdateHostIntervals(t *testing.T) { LoggerTLSPeriod: 33, ConfigTLSRefresh: 60, }, - map[string]interface{}{ + json.RawMessage(`{"options":{ "distributed_interval": 11, - "logger_tls_period": 33, - }, + "logger_tls_period": 33 + }}`), false, }, } for _, tt := range testCases { - ds.FIMSectionsFuncInvoked = false - t.Run("", func(t *testing.T) { ctx := hostctx.NewContext(context.Background(), tt.initHost) - ds.GetOsqueryConfigOptionsFunc = func() (map[string]interface{}, error) { + ds.OptionsForPlatformFunc = func(platform string) (json.RawMessage, error) { return tt.configOptions, nil } @@ -1097,22 +1124,9 @@ func TestUpdateHostIntervals(t *testing.T) { return nil } - cfg, err := svc.GetClientConfig(ctx) + _, err := svc.GetClientConfig(ctx) require.Nil(t, err) assert.Equal(t, tt.saveHostCalled, saveHostCalled) - require.True(t, ds.FIMSectionsFuncInvoked) - require.Condition(t, func() bool { - _, ok := cfg.Schedule["file_events"] - return ok - }) - assert.Equal(t, 400, int(cfg.Schedule["file_events"].Interval)) - assert.Equal(t, "SELECT * FROM file_events;", cfg.Schedule["file_events"].Query) - require.NotNil(t, cfg.FilePaths) - require.Condition(t, func() bool { - _, ok := cfg.FilePaths["etc"] - return ok - }) - assert.Len(t, cfg.FilePaths["etc"], 1) }) } diff --git a/server/service/service_packs.go b/server/service/service_packs.go index e88cc21fb..8eda79155 100644 --- a/server/service/service_packs.go +++ b/server/service/service_packs.go @@ -3,10 +3,21 @@ package service import ( "context" - "github.com/kolide/fleet/server/contexts/viewer" "github.com/kolide/fleet/server/kolide" ) +func (svc service) ApplyPackSpecs(ctx context.Context, specs []*kolide.PackSpec) error { + return svc.ds.ApplyPackSpecs(specs) +} + +func (svc service) GetPackSpecs(ctx context.Context) ([]*kolide.PackSpec, error) { + return svc.ds.GetPackSpecs() +} + +func (svc service) GetPackSpec(ctx context.Context, name string) (*kolide.PackSpec, error) { + return svc.ds.GetPackSpec(name) +} + func (svc service) ListPacks(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Pack, error) { return svc.ds.ListPacks(opt) } @@ -15,208 +26,14 @@ func (svc service) GetPack(ctx context.Context, id uint) (*kolide.Pack, error) { return svc.ds.Pack(id) } -func (svc service) NewPack(ctx context.Context, p kolide.PackPayload) (*kolide.Pack, error) { - var pack kolide.Pack - - if p.Name != nil { - pack.Name = *p.Name - } - - if p.Description != nil { - pack.Description = *p.Description - } - - if p.Platform != nil { - pack.Platform = *p.Platform - } - - if p.Disabled != nil { - pack.Disabled = *p.Disabled - } - - vc, ok := viewer.FromContext(ctx) - if ok { - if createdBy := vc.UserID(); createdBy != uint(0) { - pack.CreatedBy = createdBy - } - } - - _, err := svc.ds.NewPack(&pack) - if err != nil { - return nil, err - } - - if p.HostIDs != nil { - for _, hostID := range *p.HostIDs { - err = svc.AddHostToPack(ctx, hostID, pack.ID) - if err != nil { - return nil, err - } - } - } - - if p.LabelIDs != nil { - for _, labelID := range *p.LabelIDs { - err = svc.AddLabelToPack(ctx, labelID, pack.ID) - if err != nil { - return nil, err - } - } - } - - return &pack, nil -} - -func (svc service) ModifyPack(ctx context.Context, id uint, p kolide.PackPayload) (*kolide.Pack, error) { - pack, err := svc.ds.Pack(id) - if err != nil { - return nil, err - } - - if p.Name != nil { - pack.Name = *p.Name - } - - if p.Description != nil { - pack.Description = *p.Description - } - - if p.Platform != nil { - pack.Platform = *p.Platform - } - - if p.Disabled != nil { - pack.Disabled = *p.Disabled - } - - err = svc.ds.SavePack(pack) - if err != nil { - return nil, err - } - - // we must determine what hosts are attached to this pack. then, given - // our new set of host_ids, we will mutate the database to reflect the - // desired state. - if p.HostIDs != nil { - - // first, let's retrieve the total set of hosts - hosts, err := svc.ListHostsInPack(ctx, pack.ID, kolide.ListOptions{}) - if err != nil { - return nil, err - } - - // it will be efficient to create a data structure with constant time - // lookups to determine whether or not a host is already added - existingHosts := map[uint]bool{} - for _, host := range hosts { - existingHosts[host] = true - } - - // we will also make a constant time lookup map for the desired set of - // hosts as well. - desiredHosts := map[uint]bool{} - for _, hostID := range *p.HostIDs { - desiredHosts[hostID] = true - } - - // if the request declares a host ID but the host is not already - // associated with the pack, we add it - for _, hostID := range *p.HostIDs { - if !existingHosts[hostID] { - err = svc.AddHostToPack(ctx, hostID, pack.ID) - if err != nil { - return nil, err - } - } - } - - // if the request does not declare the ID of a host which currently - // exists, we delete the existing relationship - for hostID := range existingHosts { - if !desiredHosts[hostID] { - err = svc.RemoveHostFromPack(ctx, hostID, pack.ID) - if err != nil { - return nil, err - } - } - } - } - - // we must determine what labels are attached to this pack. then, given - // our new set of label_ids, we will mutate the database to reflect the - // desired state. - if p.LabelIDs != nil { - - // first, let's retrieve the total set of labels - labels, err := svc.ListLabelsForPack(ctx, pack.ID) - if err != nil { - return nil, err - } - - // it will be efficient to create a data structure with constant time - // lookups to determine whether or not a label is already added - existingLabels := map[uint]bool{} - for _, label := range labels { - existingLabels[label.ID] = true - } - - // we will also make a constant time lookup map for the desired set of - // labels as well. - desiredLabels := map[uint]bool{} - for _, labelID := range *p.LabelIDs { - desiredLabels[labelID] = true - } - - // if the request declares a label ID but the label is not already - // associated with the pack, we add it - for _, labelID := range *p.LabelIDs { - if !existingLabels[labelID] { - err = svc.AddLabelToPack(ctx, labelID, pack.ID) - if err != nil { - return nil, err - } - } - } - - // if the request does not declare the ID of a label which currently - // exists, we delete the existing relationship - for labelID := range existingLabels { - if !desiredLabels[labelID] { - err = svc.RemoveLabelFromPack(ctx, labelID, pack.ID) - if err != nil { - return nil, err - } - } - } - } - - return pack, err -} - -func (svc service) DeletePack(ctx context.Context, id uint) error { - return svc.ds.DeletePack(id) -} - -func (svc service) AddLabelToPack(ctx context.Context, lid, pid uint) error { - return svc.ds.AddLabelToPack(lid, pid) +func (svc service) DeletePack(ctx context.Context, name string) error { + return svc.ds.DeletePack(name) } func (svc service) ListLabelsForPack(ctx context.Context, pid uint) ([]*kolide.Label, error) { return svc.ds.ListLabelsForPack(pid) } -func (svc service) RemoveLabelFromPack(ctx context.Context, lid, pid uint) error { - return svc.ds.RemoveLabelFromPack(lid, pid) -} - -func (svc service) AddHostToPack(ctx context.Context, hid, pid uint) error { - return svc.ds.AddHostToPack(hid, pid) -} - -func (svc service) RemoveHostFromPack(ctx context.Context, hid, pid uint) error { - return svc.ds.RemoveHostFromPack(hid, pid) -} - func (svc service) ListHostsInPack(ctx context.Context, pid uint, opt kolide.ListOptions) ([]uint, error) { return svc.ds.ListHostsInPack(pid, opt) } @@ -226,65 +43,5 @@ func (svc service) ListExplicitHostsInPack(ctx context.Context, pid uint, opt ko } func (svc service) ListPacksForHost(ctx context.Context, hid uint) ([]*kolide.Pack, error) { - packs := []*kolide.Pack{} - - // we will need to give some subset of packs to this host based on the - // labels which this host is known to belong to - allPacks, err := svc.ds.ListPacks(kolide.ListOptions{}) - if err != nil { - return nil, err - } - - // pull the labels that this host belongs to - labels, err := svc.ds.ListLabelsForHost(hid) - if err != nil { - return nil, err - } - - // in order to use o(1) array indexing in an o(n) loop vs a o(n^2) double - // for loop iteration, we must create the array which may be indexed below - labelIDs := map[uint]bool{} - for _, label := range labels { - labelIDs[label.ID] = true - } - - for _, pack := range allPacks { - // don't include packs which have been disabled - if pack.Disabled { - continue - } - - // for each pack, we must know what labels have been assigned to that - // pack - labelsForPack, err := svc.ds.ListLabelsForPack(pack.ID) - if err != nil { - return nil, err - } - - // o(n) iteration to determine whether or not a pack is enabled - // in this case, n is len(labelsForPack) - for _, label := range labelsForPack { - if labelIDs[label.ID] { - packs = append(packs, pack) - break - } - } - - // for each pack, we must know what host have been assigned to that pack - hostsForPack, err := svc.ds.ListExplicitHostsInPack(pack.ID, kolide.ListOptions{}) - if err != nil { - return nil, err - } - - // o(n) iteration to determine whether or not a pack is enabled - // in this case, n is len(hostsForPack) - for _, host := range hostsForPack { - if host == hid { - packs = append(packs, pack) - break - } - } - } - - return packs, nil + return svc.ds.ListPacksForHost(hid) } diff --git a/server/service/service_packs_test.go b/server/service/service_packs_test.go index ec5ae1e58..2272f679b 100644 --- a/server/service/service_packs_test.go +++ b/server/service/service_packs_test.go @@ -4,13 +4,10 @@ import ( "context" "testing" - "github.com/WatchBeam/clock" "github.com/kolide/fleet/server/config" "github.com/kolide/fleet/server/datastore/inmem" "github.com/kolide/fleet/server/kolide" - "github.com/kolide/fleet/server/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestListPacks(t *testing.T) { @@ -57,151 +54,3 @@ func TestGetPack(t *testing.T) { assert.Equal(t, pack.ID, packVerify.ID) } - -func TestNewPack(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - labelName := "label" - labelQuery := "select 1" - label, err := svc.NewLabel(ctx, kolide.LabelPayload{ - Name: &labelName, - Query: &labelQuery, - }) - - packName := "foo" - packLabelIDs := []uint{label.ID} - pack, err := svc.NewPack(ctx, kolide.PackPayload{ - Name: &packName, - LabelIDs: &packLabelIDs, - }) - - assert.Nil(t, err) - - packs, err := ds.ListPacks(kolide.ListOptions{}) - assert.Nil(t, err) - require.Len(t, packs, 1) - assert.Equal(t, pack.ID, packs[0].ID) - - labels, err := ds.ListLabelsForPack(pack.ID) - assert.Nil(t, err) - require.Len(t, labels, 1) - assert.Equal(t, label.ID, labels[0].ID) -} - -func TestModifyPack(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - label := &kolide.Label{ - Name: "label", - Query: "select 1", - } - label, err = ds.NewLabel(label) - assert.Nil(t, err) - assert.NotZero(t, label.ID) - - pack := &kolide.Pack{ - Name: "foo", - } - pack, err = ds.NewPack(pack) - assert.Nil(t, err) - assert.NotZero(t, pack.ID) - - newName := "bar" - labelIDs := []uint{label.ID} - packVerify, err := svc.ModifyPack(ctx, pack.ID, kolide.PackPayload{ - Name: &newName, - LabelIDs: &labelIDs, - }) - assert.Nil(t, err) - - assert.Equal(t, pack.ID, packVerify.ID) - assert.Equal(t, "bar", packVerify.Name) - - labels, err := ds.ListLabelsForPack(pack.ID) - assert.Nil(t, err) - require.Len(t, labels, 1) - assert.Equal(t, label.ID, labels[0].ID) - - newLabelIDs := []uint{} - packVerify2, err := svc.ModifyPack(ctx, pack.ID, kolide.PackPayload{ - LabelIDs: &newLabelIDs, - }) - assert.Nil(t, err) - - assert.Equal(t, pack.ID, packVerify2.ID) - - labels, err = ds.ListLabelsForPack(pack.ID) - assert.Nil(t, err) - require.Len(t, labels, 0) -} - -func TestDeletePack(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - pack := &kolide.Pack{ - Name: "foo", - } - _, err = ds.NewPack(pack) - assert.Nil(t, err) - assert.NotZero(t, pack.ID) - - err = svc.DeletePack(ctx, pack.ID) - assert.Nil(t, err) - - queries, err := ds.ListPacks(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, queries, 0) -} - -func TestListPacksForHost(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - mockClock := clock.NewMockClock() - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - h1 := test.NewHost(t, ds, "h1", "10.10.10.1", "1", "1", mockClock.Now()) - h2 := test.NewHost(t, ds, "h2", "10.10.10.2", "2", "2", mockClock.Now()) - - p1 := test.NewPack(t, ds, "p1") - p2 := test.NewPack(t, ds, "p2") - - require.Nil(t, svc.AddHostToPack(ctx, h1.ID, p1.ID)) - require.Nil(t, svc.AddHostToPack(ctx, h2.ID, p1.ID)) - - require.Nil(t, svc.AddHostToPack(ctx, h1.ID, p2.ID)) - - { - packs, err := svc.ListPacksForHost(ctx, h1.ID) - require.Nil(t, err) - require.Len(t, packs, 2) - } - { - packs, err := svc.ListPacksForHost(ctx, h2.ID) - require.Nil(t, err) - require.Len(t, packs, 1) - } - -} diff --git a/server/service/service_queries.go b/server/service/service_queries.go index 7d4fcc389..8f24ec0ba 100644 --- a/server/service/service_queries.go +++ b/server/service/service_queries.go @@ -5,8 +5,61 @@ import ( "github.com/kolide/fleet/server/contexts/viewer" "github.com/kolide/fleet/server/kolide" + "github.com/pkg/errors" ) +func queryFromSpec(spec *kolide.QuerySpec) *kolide.Query { + return &kolide.Query{ + Name: spec.Name, + Description: spec.Description, + Query: spec.Query, + } +} + +func specFromQuery(query *kolide.Query) *kolide.QuerySpec { + return &kolide.QuerySpec{ + Name: query.Name, + Description: query.Description, + Query: query.Query, + } +} + +func (svc service) ApplyQuerySpecs(ctx context.Context, specs []*kolide.QuerySpec) error { + vc, ok := viewer.FromContext(ctx) + if !ok { + return errors.New("user must be authenticated to apply queries") + } + + queries := []*kolide.Query{} + for _, spec := range specs { + queries = append(queries, queryFromSpec(spec)) + } + + err := svc.ds.ApplyQueries(vc.UserID(), queries) + return errors.Wrap(err, "applying queries") +} + +func (svc service) GetQuerySpecs(ctx context.Context) ([]*kolide.QuerySpec, error) { + queries, err := svc.ds.ListQueries(kolide.ListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "getting queries") + } + + specs := []*kolide.QuerySpec{} + for _, query := range queries { + specs = append(specs, specFromQuery(query)) + } + return specs, nil +} + +func (svc service) GetQuerySpec(ctx context.Context, name string) (*kolide.QuerySpec, error) { + query, err := svc.ds.QueryByName(name) + if err != nil { + return nil, err + } + return specFromQuery(query), nil +} + func (svc service) ListQueries(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Query, error) { return svc.ds.ListQueries(opt) } @@ -70,8 +123,8 @@ func (svc service) ModifyQuery(ctx context.Context, id uint, p kolide.QueryPaylo return query, nil } -func (svc service) DeleteQuery(ctx context.Context, id uint) error { - return svc.ds.DeleteQuery(id) +func (svc service) DeleteQuery(ctx context.Context, name string) error { + return svc.ds.DeleteQuery(name) } func (svc service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) { diff --git a/server/service/service_queries_test.go b/server/service/service_queries_test.go index 42904aa26..923209639 100644 --- a/server/service/service_queries_test.go +++ b/server/service/service_queries_test.go @@ -117,28 +117,3 @@ func TestModifyQuery(t *testing.T) { assert.Equal(t, query.ID, queryVerify.ID) assert.Equal(t, "bar", queryVerify.Name) } - -func TestDeleteQuery(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - - ctx := context.Background() - - query := &kolide.Query{ - Name: "foo", - Query: "select * from time;", - } - query, err = ds.NewQuery(query) - assert.Nil(t, err) - assert.NotZero(t, query.ID) - - err = svc.DeleteQuery(ctx, query.ID) - assert.Nil(t, err) - - queries, err := ds.ListQueries(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, queries, 0) -} diff --git a/server/service/service_scheduled_queries.go b/server/service/service_scheduled_queries.go index ce570699c..b09c6cd98 100644 --- a/server/service/service_scheduled_queries.go +++ b/server/service/service_scheduled_queries.go @@ -4,62 +4,8 @@ import ( "context" "github.com/kolide/fleet/server/kolide" - "github.com/pkg/errors" ) -func (svc service) GetScheduledQuery(ctx context.Context, id uint) (*kolide.ScheduledQuery, error) { - return svc.ds.ScheduledQuery(id) -} - func (svc service) GetScheduledQueriesInPack(ctx context.Context, id uint, opts kolide.ListOptions) ([]*kolide.ScheduledQuery, error) { return svc.ds.ListScheduledQueriesInPack(id, opts) } - -func (svc service) ScheduleQuery(ctx context.Context, sq *kolide.ScheduledQuery) (*kolide.ScheduledQuery, error) { - return svc.ds.NewScheduledQuery(sq) -} - -func (svc service) ModifyScheduledQuery(ctx context.Context, id uint, p kolide.ScheduledQueryPayload) (*kolide.ScheduledQuery, error) { - sq, err := svc.GetScheduledQuery(ctx, id) - if err != nil { - return nil, errors.Wrap(err, "getting scheduled query to modify") - } - - if p.PackID != nil { - sq.PackID = *p.PackID - } - - if p.QueryID != nil { - sq.QueryID = *p.QueryID - } - - if p.Interval != nil { - sq.Interval = *p.Interval - } - - if p.Snapshot != nil { - sq.Snapshot = p.Snapshot - } - - if p.Removed != nil { - sq.Removed = p.Removed - } - - if p.Platform != nil { - sq.Platform = p.Platform - } - - if p.Version != nil { - sq.Version = p.Version - } - - if p.Shard != nil { - sq.Shard = p.Shard - } - - return svc.ds.SaveScheduledQuery(sq) -} - -func (svc service) DeleteScheduledQuery(ctx context.Context, id uint) error { - return svc.ds.DeleteScheduledQuery(id) -} diff --git a/server/service/service_scheduled_queries_test.go b/server/service/service_scheduled_queries_test.go deleted file mode 100644 index 6a1888c55..000000000 --- a/server/service/service_scheduled_queries_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package service - -import ( - "context" - "testing" - - "github.com/kolide/fleet/server/config" - "github.com/kolide/fleet/server/datastore/inmem" - "github.com/kolide/fleet/server/kolide" - "github.com/kolide/fleet/server/test" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetScheduledQueriesInPack(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - ctx := context.Background() - - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - q2 := test.NewQuery(t, ds, "bar", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - queries, err := svc.GetScheduledQueriesInPack(ctx, p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, queries, 1) - assert.Equal(t, sq1.ID, queries[0].ID) - - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, false, false) - test.NewScheduledQuery(t, ds, p1.ID, q2.ID, 60, true, false) - - queries, err = svc.GetScheduledQueriesInPack(ctx, p1.ID, kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, queries, 3) -} - -func TestGetScheduledQuery(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - ctx := context.Background() - - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := svc.GetScheduledQuery(ctx, sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) -} - -func TestModifyScheduledQuery(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - ctx := context.Background() - - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := svc.GetScheduledQuery(ctx, sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) - - interval := uint(120) - queryPayload := kolide.ScheduledQueryPayload{ - Interval: &interval, - } - query, err = svc.ModifyScheduledQuery(ctx, sq1.ID, queryPayload) - assert.Equal(t, uint(120), query.Interval) - - queryVerify, err := svc.GetScheduledQuery(ctx, sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(120), queryVerify.Interval) -} - -func TestDeleteScheduledQuery(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - assert.Nil(t, err) - svc, err := newTestService(ds, nil) - assert.Nil(t, err) - ctx := context.Background() - - u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) - p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) - - query, err := svc.GetScheduledQuery(ctx, sq1.ID) - require.Nil(t, err) - assert.Equal(t, uint(60), query.Interval) - - err = svc.DeleteScheduledQuery(ctx, sq1.ID) - require.Nil(t, err) - - _, err = svc.GetScheduledQuery(ctx, sq1.ID) - require.NotNil(t, err) -} diff --git a/server/service/transport.go b/server/service/transport.go index dc43f50f4..edec25823 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -68,6 +68,15 @@ func idFromRequest(r *http.Request, name string) (uint, error) { return uint(uid), nil } +func nameFromRequest(r *http.Request, varName string) (string, error) { + vars := mux.Vars(r) + name, ok := vars[varName] + if !ok { + return "", errBadRoute + } + return name, nil +} + // default number of items to include per page const defaultPerPage = 20 @@ -141,3 +150,17 @@ func listOptionsFromRequest(r *http.Request) (kolide.ListOptions, error) { func decodeNoParamsRequest(ctx context.Context, r *http.Request) (interface{}, error) { return nil, nil } + +type getGenericSpecRequest struct { + Name string +} + +func decodeGetGenericSpecRequest(ctx context.Context, r *http.Request) (interface{}, error) { + name, err := nameFromRequest(r, "name") + if err != nil { + return nil, err + } + var req getGenericSpecRequest + req.Name = name + return req, nil +} diff --git a/server/service/transport_campaigns.go b/server/service/transport_campaigns.go index e9f4efe09..f42fa1f82 100644 --- a/server/service/transport_campaigns.go +++ b/server/service/transport_campaigns.go @@ -13,3 +13,11 @@ func decodeCreateDistributedQueryCampaignRequest(ctx context.Context, r *http.Re } return req, nil } + +func decodeCreateDistributedQueryCampaignByNamesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req createDistributedQueryCampaignByNamesRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +} diff --git a/server/service/transport_decorators.go b/server/service/transport_decorators.go deleted file mode 100644 index 8672edb85..000000000 --- a/server/service/transport_decorators.go +++ /dev/null @@ -1,38 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "net/http" -) - -func decodeNewDecoratorRequest(ctx context.Context, req *http.Request) (interface{}, error) { - var dec newDecoratorRequest - err := json.NewDecoder(req.Body).Decode(&dec) - if err != nil { - return nil, err - } - return dec, nil -} - -func decodeDeleteDecoratorRequest(ctx context.Context, req *http.Request) (interface{}, error) { - id, err := idFromRequest(req, "id") - if err != nil { - return nil, err - } - return deleteDecoratorRequest{ID: id}, nil -} - -func decodeModifyDecoratorRequest(ctx context.Context, req *http.Request) (interface{}, error) { - var request newDecoratorRequest - id, err := idFromRequest(req, "id") - if err != nil { - return nil, err - } - err = json.NewDecoder(req.Body).Decode(&request) - if err != nil { - return nil, err - } - request.Payload.ID = id - return request, nil -} diff --git a/server/service/transport_import_config.go b/server/service/transport_import_config.go deleted file mode 100644 index a0292547b..000000000 --- a/server/service/transport_import_config.go +++ /dev/null @@ -1,35 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/kolide/fleet/server/kolide" -) - -func decodeImportConfigRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req importRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, err - } - // Unmarshal main config - conf := kolide.ImportConfig{ - DryRun: req.DryRun, - Packs: make(kolide.PackNameMap), - ExternalPacks: make(kolide.PackNameToPackDetails), - } - if err := json.Unmarshal([]byte(req.Config), &conf); err != nil { - return nil, err - } - // Unmarshal external packs - for packName, packConfig := range req.ExternalPackConfigs { - var pack kolide.PackDetails - if err := json.Unmarshal([]byte(packConfig), &pack); err != nil { - return nil, err - } - conf.ExternalPacks[packName] = pack - } - conf.GlobPackNames = req.GlobPackNames - return conf, nil -} diff --git a/server/service/transport_labels.go b/server/service/transport_labels.go index c4321d6e9..ae8837738 100644 --- a/server/service/transport_labels.go +++ b/server/service/transport_labels.go @@ -6,21 +6,13 @@ import ( "net/http" ) -func decodeCreateLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req createLabelRequest - if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { - return nil, err - } - return req, nil -} - func decodeDeleteLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") + name, err := nameFromRequest(r, "name") if err != nil { return nil, err } var req deleteLabelRequest - req.ID = id + req.Name = name return req, nil } @@ -42,16 +34,11 @@ func decodeListLabelsRequest(ctx context.Context, r *http.Request) (interface{}, return listLabelsRequest{ListOptions: opt}, nil } -func decodeModifyLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") - if err != nil { +func decodeApplyLabelSpecsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req applyLabelSpecsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, err } - var resp modifyLabelRequest - err = json.NewDecoder(r.Body).Decode(&resp.payload) - if err != nil { - return nil, err - } - resp.ID = id - return resp, nil + return req, nil + } diff --git a/server/service/transport_labels_test.go b/server/service/transport_labels_test.go index e15b28343..7ba50d115 100644 --- a/server/service/transport_labels_test.go +++ b/server/service/transport_labels_test.go @@ -1,7 +1,6 @@ package service import ( - "bytes" "context" "net/http" "net/http/httptest" @@ -11,44 +10,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDecodeCreateLabelRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/labels", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeCreateLabelRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(createLabelRequest) - assert.Equal(t, "foo", *params.payload.Name) - assert.Equal(t, "select * from foo;", *params.payload.Query) - assert.Equal(t, "darwin", *params.payload.Platform) - }).Methods("POST") - - var body bytes.Buffer - body.Write([]byte(`{ - "name": "foo", - "query": "select * from foo;", - "platform": "darwin" - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/kolide/labels", &body), - ) -} - func TestDecodeDeleteLabelRequest(t *testing.T) { router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/labels/{id}", func(writer http.ResponseWriter, request *http.Request) { + router.HandleFunc("/api/v1/kolide/labels/{name}", func(writer http.ResponseWriter, request *http.Request) { r, err := decodeDeleteLabelRequest(context.Background(), request) assert.Nil(t, err) params := r.(deleteLabelRequest) - assert.Equal(t, uint(1), params.ID) + assert.Equal(t, "foo", params.Name) }).Methods("DELETE") router.ServeHTTP( httptest.NewRecorder(), - httptest.NewRequest("DELETE", "/api/v1/kolide/labels/1", nil), + httptest.NewRequest("DELETE", "/api/v1/kolide/labels/foo", nil), ) } diff --git a/server/service/transport_osquery_options.go b/server/service/transport_osquery_options.go new file mode 100644 index 000000000..3b46ec4de --- /dev/null +++ b/server/service/transport_osquery_options.go @@ -0,0 +1,16 @@ +package service + +import ( + "context" + "encoding/json" + "net/http" +) + +func decodeApplyOsqueryOptionsSpecRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req applyOsqueryOptionsSpecRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil + +} diff --git a/server/service/transport_packs.go b/server/service/transport_packs.go index 6ccc63b82..540c1ba41 100644 --- a/server/service/transport_packs.go +++ b/server/service/transport_packs.go @@ -6,35 +6,13 @@ import ( "net/http" ) -func decodeCreatePackRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req createPackRequest - if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { - return nil, err - } - - return req, nil -} - -func decodeModifyPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") - if err != nil { - return nil, err - } - var req modifyPackRequest - if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { - return nil, err - } - req.ID = id - return req, nil -} - func decodeDeletePackRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") + name, err := nameFromRequest(r, "name") if err != nil { return nil, err } var req deletePackRequest - req.ID = id + req.Name = name return req, nil } @@ -55,3 +33,12 @@ func decodeListPacksRequest(ctx context.Context, r *http.Request) (interface{}, } return listPacksRequest{ListOptions: opt}, nil } + +func decodeApplyPackSpecsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req applyPackSpecsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil + +} diff --git a/server/service/transport_packs_test.go b/server/service/transport_packs_test.go index 0806e4c5b..a4f5fdbc5 100644 --- a/server/service/transport_packs_test.go +++ b/server/service/transport_packs_test.go @@ -1,7 +1,6 @@ package service import ( - "bytes" "context" "net/http" "net/http/httptest" @@ -9,81 +8,21 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestDecodeCreatePackRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/packs", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeCreatePackRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(createPackRequest) - assert.Equal(t, "foo", *params.payload.Name) - assert.Equal(t, "bar", *params.payload.Description) - require.NotNil(t, params.payload.HostIDs) - assert.Len(t, *params.payload.HostIDs, 3) - require.NotNil(t, params.payload.LabelIDs) - assert.Len(t, *params.payload.LabelIDs, 2) - }).Methods("POST") - - var body bytes.Buffer - body.Write([]byte(`{ - "name": "foo", - "description": "bar", - "host_ids": [1, 2, 3], - "label_ids": [1, 5] - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/kolide/packs", &body), - ) -} - -func TestDecodeModifyPackRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/packs/{id}", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeModifyPackRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(modifyPackRequest) - assert.Equal(t, uint(1), params.ID) - assert.Equal(t, "foo", *params.payload.Name) - assert.Equal(t, "bar", *params.payload.Description) - require.NotNil(t, params.payload.HostIDs) - assert.Len(t, *params.payload.HostIDs, 3) - require.NotNil(t, params.payload.LabelIDs) - assert.Len(t, *params.payload.LabelIDs, 2) - }).Methods("PATCH") - - var body bytes.Buffer - body.Write([]byte(`{ - "name": "foo", - "description": "bar", - "host_ids": [1, 2, 3], - "label_ids": [1, 5] - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("PATCH", "/api/v1/kolide/packs/1", &body), - ) -} - func TestDecodeDeletePackRequest(t *testing.T) { router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/packs/{id}", func(writer http.ResponseWriter, request *http.Request) { + router.HandleFunc("/api/v1/kolide/packs/{name}", func(writer http.ResponseWriter, request *http.Request) { r, err := decodeDeletePackRequest(context.Background(), request) assert.Nil(t, err) params := r.(deletePackRequest) - assert.Equal(t, uint(1), params.ID) + assert.Equal(t, "packaday", params.Name) }).Methods("DELETE") router.ServeHTTP( httptest.NewRecorder(), - httptest.NewRequest("DELETE", "/api/v1/kolide/packs/1", nil), + httptest.NewRequest("DELETE", "/api/v1/kolide/packs/packaday", nil), ) } diff --git a/server/service/transport_queries.go b/server/service/transport_queries.go index 5f5cd53e8..bb70daa0c 100644 --- a/server/service/transport_queries.go +++ b/server/service/transport_queries.go @@ -28,12 +28,12 @@ func decodeModifyQueryRequest(ctx context.Context, r *http.Request) (interface{} } func decodeDeleteQueryRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") + name, err := nameFromRequest(r, "name") if err != nil { return nil, err } var req deleteQueryRequest - req.ID = id + req.Name = name return req, nil } @@ -62,3 +62,12 @@ func decodeListQueriesRequest(ctx context.Context, r *http.Request) (interface{} } return listQueriesRequest{ListOptions: opt}, nil } + +func decodeApplyQuerySpecsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req applyQuerySpecsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil + +} diff --git a/server/service/transport_queries_test.go b/server/service/transport_queries_test.go index 393eb69de..fe6b7e9c4 100644 --- a/server/service/transport_queries_test.go +++ b/server/service/transport_queries_test.go @@ -58,17 +58,17 @@ func TestDecodeModifyQueryRequest(t *testing.T) { func TestDecodeDeleteQueryRequest(t *testing.T) { router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/queries/{id}", func(writer http.ResponseWriter, request *http.Request) { + router.HandleFunc("/api/v1/kolide/queries/{name}", func(writer http.ResponseWriter, request *http.Request) { r, err := decodeDeleteQueryRequest(context.Background(), request) assert.Nil(t, err) params := r.(deleteQueryRequest) - assert.Equal(t, uint(1), params.ID) + assert.Equal(t, "qwerty", params.Name) }).Methods("DELETE") router.ServeHTTP( httptest.NewRecorder(), - httptest.NewRequest("DELETE", "/api/v1/kolide/queries/1", nil), + httptest.NewRequest("DELETE", "/api/v1/kolide/queries/qwerty", nil), ) } diff --git a/server/service/transport_scheduled_queries.go b/server/service/transport_scheduled_queries.go index d3a9c002c..164fdcda3 100644 --- a/server/service/transport_scheduled_queries.go +++ b/server/service/transport_scheduled_queries.go @@ -2,55 +2,9 @@ package service import ( "context" - "encoding/json" "net/http" ) -func decodeScheduleQueryRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req scheduleQueryRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, err - } - - return req, nil -} - -func decodeModifyScheduledQueryRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") - if err != nil { - return nil, err - } - var req modifyScheduledQueryRequest - - if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { - return nil, err - } - - req.ID = id - return req, nil -} - -func decodeDeleteScheduledQueryRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") - if err != nil { - return nil, err - } - var req deleteScheduledQueryRequest - req.ID = id - return req, nil -} - -func decodeGetScheduledQueryRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := idFromRequest(r, "id") - if err != nil { - return nil, err - } - var req getScheduledQueryRequest - req.ID = id - return req, nil -} - func decodeGetScheduledQueriesInPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { id, err := idFromRequest(r, "id") if err != nil { diff --git a/server/service/transport_scheduled_queries_test.go b/server/service/transport_scheduled_queries_test.go index 6e79d74c4..e97d61b02 100644 --- a/server/service/transport_scheduled_queries_test.go +++ b/server/service/transport_scheduled_queries_test.go @@ -1,7 +1,6 @@ package service import ( - "bytes" "context" "net/http" "net/http/httptest" @@ -9,98 +8,8 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestDecodeScheduleQueryRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/schedule", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeScheduleQueryRequest(context.Background(), request) - require.Nil(t, err) - - params := r.(scheduleQueryRequest) - assert.Equal(t, uint(5), params.PackID) - assert.Equal(t, uint(1), params.QueryID) - assert.Equal(t, uint(60), params.Interval) - assert.Equal(t, true, *params.Snapshot) - }).Methods("POST") - - var body bytes.Buffer - body.Write([]byte(`{ - "pack_id": 5, - "query_id": 1, - "interval": 60, - "snapshot": true - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/kolide/schedule", &body), - ) -} - -func TestDecodeModifyScheduledQueryRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/scheduled/{id}", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeModifyScheduledQueryRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(modifyScheduledQueryRequest) - assert.Equal(t, uint(1), params.ID) - assert.Equal(t, uint(5), *params.payload.PackID) - assert.Equal(t, uint(6), *params.payload.QueryID) - assert.Equal(t, true, *params.payload.Removed) - assert.Equal(t, uint(60), *params.payload.Interval) - assert.Equal(t, uint(1), *params.payload.Shard) - }).Methods("PATCH") - - var body bytes.Buffer - body.Write([]byte(`{ - "pack_id": 5, - "query_id": 6, - "removed": true, - "interval": 60, - "shard": 1 - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("PATCH", "/api/v1/kolide/scheduled/1", &body), - ) -} - -func TestDecodeDeleteScheduledQueryRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/scheduled/{id}", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeDeleteScheduledQueryRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(deleteScheduledQueryRequest) - assert.Equal(t, uint(1), params.ID) - }).Methods("DELETE") - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("DELETE", "/api/v1/kolide/scheduled/1", nil), - ) -} - -func TestDecodeGetScheduledQueryRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/kolide/scheduled/{id}", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeGetScheduledQueryRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(getScheduledQueryRequest) - assert.Equal(t, uint(1), params.ID) - }).Methods("GET") - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("GET", "/api/v1/kolide/scheduled/1", nil), - ) -} - func TestDecodeGetScheduledQueriesInPackRequest(t *testing.T) { router := mux.NewRouter() router.HandleFunc("/api/v1/kolide/packs/{id}/scheduled", func(writer http.ResponseWriter, request *http.Request) { diff --git a/server/service/validation_decorators.go b/server/service/validation_decorators.go deleted file mode 100644 index 70bb11909..000000000 --- a/server/service/validation_decorators.go +++ /dev/null @@ -1,104 +0,0 @@ -package service - -import ( - "context" - - "github.com/kolide/fleet/server/kolide" -) - -func validateNewDecoratorType(payload kolide.DecoratorPayload, invalid *invalidArgumentError) { - if payload.DecoratorType == nil { - invalid.Append("type", "missing required argument") - return - } - if *payload.DecoratorType == kolide.DecoratorUndefined { - invalid.Append("type", "invalid value, must be load, always, or interval") - return - } - if *payload.DecoratorType == kolide.DecoratorInterval { - if payload.Interval == nil { - invalid.Append("interval", "missing required argument") - return - } - if *payload.Interval%60 != 0 { - invalid.Append("interval", "must be divisible by 60") - return - } - } -} - -// NewDecorator validator checks to make sure that a valid decorator type exists and -// if the decorator is of an interval type, an interval value is present and is -// divisable by 60 -// See https://osquery.readthedocs.io/en/stable/deployment/configuration/ -func (mw validationMiddleware) NewDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - invalid := &invalidArgumentError{} - validateNewDecoratorType(payload, invalid) - - if payload.Query == nil { - invalid.Append("query", "missing required argument") - } - - if invalid.HasErrors() { - return nil, invalid - } - return mw.Service.NewDecorator(ctx, payload) -} - -func (mw validationMiddleware) validateModifyDecoratorType(payload kolide.DecoratorPayload, invalid *invalidArgumentError) error { - if payload.DecoratorType != nil { - - if *payload.DecoratorType == kolide.DecoratorUndefined { - invalid.Append("type", "invalid value, must be load, always, or interval") - return nil - } - if *payload.DecoratorType == kolide.DecoratorInterval { - // special processing for interval type - existingDec, err := mw.ds.Decorator(payload.ID) - if err != nil { - // if decorator is not present we want to return a 404 to the client - return err - } - // if the type has changed from always or load to interval we need to - // check suitability of interval value - if existingDec.Type != kolide.DecoratorInterval { - if payload.Interval == nil { - invalid.Append("interval", "missing required argument") - return nil - } - } - } - - if payload.Interval != nil { - if *payload.Interval%60 != 0 { - invalid.Append("interval", "value must be divisible by 60") - } - } - } - return nil -} - -func (mw validationMiddleware) ModifyDecorator(ctx context.Context, payload kolide.DecoratorPayload) (*kolide.Decorator, error) { - invalid := &invalidArgumentError{} - err := mw.validateModifyDecoratorType(payload, invalid) - if err != nil { - return nil, err - } - if invalid.HasErrors() { - return nil, invalid - } - return mw.Service.ModifyDecorator(ctx, payload) -} - -func (mw validationMiddleware) DeleteDecorator(ctx context.Context, id uint) error { - invalid := &invalidArgumentError{} - dec, err := mw.ds.Decorator(id) - if err != nil { - return err - } - if dec.BuiltIn { - invalid.Append("decorator", "read only") - return invalid - } - return mw.Service.DeleteDecorator(ctx, id) -} diff --git a/server/service/validation_decorators_test.go b/server/service/validation_decorators_test.go deleted file mode 100644 index b1e09361f..000000000 --- a/server/service/validation_decorators_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package service - -import ( - "context" - "testing" - - "github.com/kolide/fleet/server/kolide" - "github.com/kolide/fleet/server/mock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var dtPtr = func(t kolide.DecoratorType) *kolide.DecoratorType { return &t } - -func TestDecoratorValidation(t *testing.T) { - ds := mock.Store{} - ds.DecoratorFunc = func(id uint) (*kolide.Decorator, error) { - return &kolide.Decorator{ - ID: 1, - Query: "select x from y;", - Type: kolide.DecoratorAlways, - }, nil - } - ds.SaveDecoratorFunc = func(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - return nil - } - svc := &service{ - ds: &ds, - } - validator := validationMiddleware{ - Service: svc, - ds: &ds, - } - - payload := kolide.DecoratorPayload{ - ID: uint(1), - DecoratorType: dtPtr(kolide.DecoratorInterval), - Interval: uintPtr(3600), - } - - dec, err := validator.ModifyDecorator(context.Background(), payload) - require.Nil(t, err) - assert.Equal(t, kolide.DecoratorInterval, dec.Type) - assert.Equal(t, uint(3600), dec.Interval) -} - -func TestDecoratorValidationIntervalMissing(t *testing.T) { - ds := mock.Store{} - ds.DecoratorFunc = func(id uint) (*kolide.Decorator, error) { - return &kolide.Decorator{ - ID: 1, - Query: "select x from y;", - Type: kolide.DecoratorAlways, - }, nil - } - ds.SaveDecoratorFunc = func(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - return nil - } - svc := &service{ - ds: &ds, - } - validator := validationMiddleware{ - Service: svc, - ds: &ds, - } - - payload := kolide.DecoratorPayload{ - ID: uint(1), - DecoratorType: dtPtr(kolide.DecoratorInterval), - } - - _, err := validator.ModifyDecorator(context.Background(), payload) - require.NotNil(t, err) - r, ok := err.(*invalidArgumentError) - require.True(t, ok) - assert.Equal(t, "missing required argument", (*r)[0].reason) -} - -func TestDecoratorValidationIntervalSameType(t *testing.T) { - ds := mock.Store{} - ds.DecoratorFunc = func(id uint) (*kolide.Decorator, error) { - return &kolide.Decorator{ - ID: 1, - Query: "select x from y;", - Type: kolide.DecoratorInterval, - Interval: 600, - }, nil - } - ds.SaveDecoratorFunc = func(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - return nil - } - svc := &service{ - ds: &ds, - } - validator := validationMiddleware{ - Service: svc, - ds: &ds, - } - - payload := kolide.DecoratorPayload{ - ID: uint(1), - DecoratorType: dtPtr(kolide.DecoratorInterval), - Interval: uintPtr(1200), - } - - dec, err := validator.ModifyDecorator(context.Background(), payload) - require.Nil(t, err) - assert.Equal(t, uint(1200), dec.Interval) -} - -func TestDecoratorValidationIntervalInvalid(t *testing.T) { - ds := mock.Store{} - ds.DecoratorFunc = func(id uint) (*kolide.Decorator, error) { - return &kolide.Decorator{ - ID: 1, - Query: "select x from y;", - Type: kolide.DecoratorInterval, - Interval: 600, - }, nil - } - ds.SaveDecoratorFunc = func(dec *kolide.Decorator, opts ...kolide.OptionalArg) error { - return nil - } - svc := &service{ - ds: &ds, - } - validator := validationMiddleware{ - Service: svc, - ds: &ds, - } - - payload := kolide.DecoratorPayload{ - ID: uint(1), - DecoratorType: dtPtr(kolide.DecoratorInterval), - Interval: uintPtr(1203), - } - - _, err := validator.ModifyDecorator(context.Background(), payload) - require.NotNil(t, err) - r, ok := err.(*invalidArgumentError) - require.True(t, ok) - assert.Equal(t, "value must be divisible by 60", (*r)[0].reason) -} diff --git a/server/service/validation_import_config.go b/server/service/validation_import_config.go deleted file mode 100644 index 98c247611..000000000 --- a/server/service/validation_import_config.go +++ /dev/null @@ -1,127 +0,0 @@ -package service - -import ( - "context" - "strconv" - - "github.com/kolide/fleet/server/kolide" -) - -func (vm validationMiddleware) ImportConfig(ctx context.Context, cfg *kolide.ImportConfig) (*kolide.ImportConfigResponse, error) { - var invalid invalidArgumentError - vm.validateConfigOptions(cfg, &invalid) - vm.validatePacks(cfg, &invalid) - vm.validateDecorator(cfg, &invalid) - vm.validateYARA(cfg, &invalid) - if invalid.HasErrors() { - return nil, invalid - } - return vm.Service.ImportConfig(ctx, cfg) -} - -func (vm validationMiddleware) validateYARA(cfg *kolide.ImportConfig, argErrs *invalidArgumentError) { - if cfg.YARA != nil { - if cfg.YARA.FilePaths == nil { - argErrs.Append("yara", "missing file_paths") - return - } - if cfg.YARA.Signatures == nil { - argErrs.Append("yara", "missing signatures") - } - for fileSection, sigs := range cfg.YARA.FilePaths { - if cfg.FileIntegrityMonitoring == nil { - argErrs.Append("yara", "missing file paths section") - return - } - if _, ok := cfg.FileIntegrityMonitoring[fileSection]; !ok { - argErrs.Appendf("yara", "missing referenced file_paths section '%s'", fileSection) - } - for _, sig := range sigs { - if _, ok := cfg.YARA.Signatures[sig]; !ok { - argErrs.Appendf( - "yara", - "missing signature '%s' referenced in '%s'", - sig, - fileSection, - ) - } - } - } - } -} - -func (vm validationMiddleware) validateDecorator(cfg *kolide.ImportConfig, argErrs *invalidArgumentError) { - if cfg.Decorators != nil { - for str := range cfg.Decorators.Interval { - val, err := strconv.ParseInt(str, 10, 32) - if err != nil { - argErrs.Appendf("decorators", "interval '%s' must be an integer", str) - continue - } - if val%60 != 0 { - - argErrs.Appendf("decorators", "interval '%d' must be divisible by 60", val) - } - } - } -} - -func (vm validationMiddleware) validateConfigOptions(cfg *kolide.ImportConfig, argErrs *invalidArgumentError) { - if cfg.Options != nil { - for optName, optValue := range cfg.Options { - opt, err := vm.ds.OptionByName(string(optName)) - if err != nil { - // skip validation for an option we don't know about, this will generate - // a warning in the service layer - continue - } - if !opt.SameType(optValue) { - argErrs.Appendf("options", "invalid type for '%s'", optName) - } - } - } -} - -func (vm validationMiddleware) validatePacks(cfg *kolide.ImportConfig, argErrs *invalidArgumentError) { - if cfg.Packs != nil { - for packName, pack := range cfg.Packs { - // if glob packs is defined we expect at least one external pack - if packName == kolide.GlobPacks { - if len(cfg.GlobPackNames) == 0 { - argErrs.Append("external_packs", "missing glob packs") - continue - } - // make sure that each glob pack has JSON content - for _, p := range cfg.GlobPackNames { - if pd, ok := cfg.ExternalPacks[p]; !ok { - argErrs.Appendf("external_packs", "missing content for '%s'", p) - } else { - vm.validatePackContents(p, pd, argErrs) - } - } - continue - } - // if value is a string we expect a file path, in this case, the user has to supply the - // contents of said file which we store in ExternalPacks, if it's not there we need to - // raise an error - switch val := pack.(type) { - case string: - if pd, ok := cfg.ExternalPacks[packName]; !ok { - argErrs.Appendf("external_packs", "missing content for '%s'", packName) - } else { - vm.validatePackContents(packName, pd, argErrs) - } - case kolide.PackDetails: - vm.validatePackContents(packName, val, argErrs) - } - } - } -} - -func (vm validationMiddleware) validatePackContents(packName string, pack kolide.PackDetails, argErrs *invalidArgumentError) { - switch pack.Platform { - case "", "darwin", "freebsd", "windows", "linux", "any", "all": - default: - argErrs.Appendf("pack", "'%s' is not a valid platform", pack.Platform) - } -} diff --git a/server/service/validation_setup.go b/server/service/validation_setup.go index 9970874ae..4f8498613 100644 --- a/server/service/validation_setup.go +++ b/server/service/validation_setup.go @@ -2,7 +2,7 @@ package service import ( "context" - "fmt" + "errors" "net/url" "github.com/kolide/fleet/server/kolide" @@ -31,7 +31,7 @@ func validateKolideServerURL(urlString string) error { return err } if serverURL.Scheme != "https" { - return fmt.Errorf("url scheme must be https") + return errors.New("url scheme must be https") } return nil } diff --git a/server/test/new_objects.go b/server/test/new_objects.go index b08d883d6..5cc2367f2 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -25,13 +25,12 @@ func NewQuery(t *testing.T, ds kolide.Datastore, name, q string, authorID uint, } func NewPack(t *testing.T, ds kolide.Datastore, name string) *kolide.Pack { - pack, err := ds.NewPack(&kolide.Pack{ - Name: name, - }) + err := ds.ApplyPackSpecs([]*kolide.PackSpec{&kolide.PackSpec{Name: name}}) require.Nil(t, err) // Loading gives us the timestamps - pack, err = ds.Pack(pack.ID) + pack, ok, err := ds.PackByName(name) + require.True(t, ok) require.Nil(t, err) return pack @@ -104,15 +103,6 @@ func NewHost(t *testing.T, ds kolide.Datastore, name, ip, key, uuid string, now return h } -func NewLabel(t *testing.T, ds kolide.Datastore, name, query string) *kolide.Label { - l, err := ds.NewLabel(&kolide.Label{Name: name, Query: query}) - - require.Nil(t, err) - require.NotZero(t, l.ID) - - return l -} - func NewUser(t *testing.T, ds kolide.Datastore, name, username, email string, admin bool) *kolide.User { u, err := ds.NewUser(&kolide.User{ Password: []byte("garbage"), @@ -128,17 +118,3 @@ func NewUser(t *testing.T, ds kolide.Datastore, name, username, email string, ad return u } - -func NewScheduledQuery(t *testing.T, ds kolide.Datastore, pid, qid, interval uint, snapshot, removed bool) *kolide.ScheduledQuery { - sq, err := ds.NewScheduledQuery(&kolide.ScheduledQuery{ - PackID: pid, - QueryID: qid, - Interval: interval, - Snapshot: &snapshot, - Removed: &removed, - }) - require.Nil(t, err) - require.NotZero(t, sq.ID) - - return sq -}