Merge pull request #1792 from kolide/fleetctl

Merge fleetctl branch into master in preparation for 2.0.0 release candidate
This commit is contained in:
Mike Arpaia 2018-05-22 15:55:35 -06:00 committed by GitHub
commit c273a92537
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
228 changed files with 6663 additions and 8945 deletions

View File

@ -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" }}

View File

@ -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!

145
Gopkg.lock generated
View File

@ -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

View File

@ -44,7 +44,7 @@
[[constraint]]
name = "github.com/igm/sockjs-go"
version = "2.0.0"
branch = "master"
[[constraint]]
branch = "master"

View File

@ -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

View File

@ -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.

47
cmd/fleetctl/api.go Normal file
View File

@ -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
}

168
cmd/fleetctl/apply.go Normal file
View File

@ -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
},
}
}

296
cmd/fleetctl/config.go Normal file
View File

@ -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
},
}
}

165
cmd/fleetctl/convert.go Normal file
View File

@ -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
},
}
}

97
cmd/fleetctl/delete.go Normal file
View File

@ -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
},
}
}

View File

@ -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()
}

270
cmd/fleetctl/get.go Normal file
View File

@ -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
},
}
}

93
cmd/fleetctl/login.go Normal file
View File

@ -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
},
}
}

38
cmd/fleetctl/logout.go Normal file
View File

@ -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
},
}
}

131
cmd/fleetctl/query.go Normal file
View File

@ -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)
}
}
},
}
}

80
cmd/fleetctl/setup.go Normal file
View File

@ -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
},
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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:

9
docs/dashboard/README.md Normal file
View File

@ -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).

View File

@ -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
```
```

22
docs/development/faq.md Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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 (
<ClickableTableRow className={rowClassName} onClick={onSelect} onDoubleClick={onDoubleClick} >
<td>
<Checkbox
name={`decorator-checkbox-${id}`}
onChange={onCheck}
value={checked}
disabled={builtIn}
/>
</td>
<td className={`${baseClass}__name`}>{name}</td>
<td className={`${baseClass}__name`}>{type}</td>
<td className={`${baseClass}__name`}>{interval}</td>
<td>{lastModifiedDate}</td>
<td className={`${baseClass}__name`}>{query}</td>
</ClickableTableRow>
);
}
}
export default DecoratorRow;

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export default from './DecoratorRow';

View File

@ -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 (
<div className={baseClass} >
<table className={`${baseClass}__table`}>
<thead>
<tr>
<th>
<Checkbox
name="check-all-decorators"
onChange={this.handleCheckAll}
value={allChecked}
/>
</th>
<th>Decorator Name</th>
<th>Type</th>
<th>Interval</th>
<th>Last Modified</th>
<th>Query</th>
</tr>
</thead>
<tbody>
{decorators.map((decorator) => {
return (
<DecoratorRow
decorator={decorator}
key={`decorator-row-${decorator.id}`}
checked={this.isChecked(decorator)}
selected={selectedDecorator && selectedDecorator.id === decorator.id}
onCheck={this.onCheck}
onSelect={onSelectDecorator}
onDoubleClick={onDoubleClick}
builtIn={decorator.built_in}
/>
);
})}
</tbody>
</table>
</div>
);
}
}
export default DecoratorRows;

View File

@ -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;
}
}
}
}
}

View File

@ -1 +0,0 @@
export default from './DecoratorRows';

View File

@ -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;

View File

@ -1 +0,0 @@
export default from './DecoratorsPageWrapper';

View File

@ -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 (
<form className={`${baseClass}__form`}>
<Button disabled={readOnly} onClick={handleRemove} variant="unstyled" className={`${baseClass}__remove`}>
<Icon name="x" onClick={handleRemove} />
</Button>
<Dropdown
{...fields.name}
className={`${baseClass}__field`}
disabled={disabled}
options={options}
/>
<InputField
{...fields.value}
disabled={readOnly}
inputClassName={`${baseClass}__field`}
type={inputType}
/>
</form>
);
}
}
export default Form(ConfigOptionForm, { fields: fieldNames });

View File

@ -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(
<ConfigOptionForm
configNameOptions={configNameOptions}
handleSubmit={noop}
onRemove={noop}
/>
);
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(
<ConfigOptionForm
configNameOptions={configNameOptions}
handleSubmit={noop}
onChangeFunc={spy}
onRemove={noop}
/>
);
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(
<ConfigOptionForm
configNameOptions={configNameOptions}
formData={formData}
handleSubmit={noop}
onRemove={noop}
/>
);
const enabledForm = mount(
<ConfigOptionForm
configNameOptions={configNameOptions}
formData={{ ...formData, value: null }}
handleSubmit={noop}
onRemove={noop}
/>
);
const readOnlyForm = mount(
<ConfigOptionForm
configNameOptions={configNameOptions}
formData={{ ...formData, read_only: true }}
handleSubmit={noop}
onRemove={noop}
/>
);
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(
<ConfigOptionForm
configNameOptions={configNameOptions}
formData={formData}
handleSubmit={noop}
onRemove={spy}
/>
);
const exIcon = form.find('Button');
exIcon.simulate('click');
expect(spy).toHaveBeenCalledWith(formData);
});
});

View File

@ -1 +0,0 @@
export default from './ConfigOptionForm';

View File

@ -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 (
<li className={`${baseClass}__option`} key={`${idx}-config-form-option`}>
<ConfigOptionForm
configNameOptions={configNameOptions}
formData={option}
key={`config-option-form-${option.id}-${idx}`}
onChangeFunc={handleFormUpdate(option)}
onRemove={onRemoveOption}
serverErrors={configErrors}
baseClass={baseClass}
/>
</li>
);
}
render () {
const { completedOptions } = this.props;
const { renderConfigOptionForm } = this;
return (
<div className={baseClass}>
<ul className={`${baseClass}__options`}>
<li className={`${baseClass}__option-header`}>
<span className={`${baseClass}__option-header-name`}>Option Name</span>
<span className={`${baseClass}__option-header-value`}>Value</span>
</li>
{completedOptions.map((option, idx) => {
return renderConfigOptionForm(option, idx);
})}
</ul>
</div>
);
}
}
export default ConfigOptionsForm;

View File

@ -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(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub]} />);
const formWithTwoOptions = mount(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub, configOptionStub]} />);
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(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub]} onFormUpdate={spy} />);
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(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub]} errors={errors} />);
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(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub]} errors={errors} />);
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(<ConfigOptionsForm configNameOptions={[]} completedOptions={[configOptionStub]} errors={errors} />);
const configOptionForm = form.find('ConfigOptionForm');
expect(configOptionForm.prop('serverErrors')).toEqual({});
});
});
});

View File

@ -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%;
}
}

View File

@ -1 +0,0 @@
export default from './ConfigOptionsForm';

View File

@ -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 (
<form className={`${baseClass}__wrapper`} onSubmit={handleSubmit} >
<h1>{formTitle}</h1>
<InputField
{...fields.name}
label="Decorator Name"
inputClassName={`${baseClass}__name`}
/>
<KolideAce
{...fields.query}
error={fields.query.error || errors.query}
label="SQL"
/>
<div className={`${baseClass}__inputs`}>
<Dropdown
{...fields.type}
options={types}
label="Decorator Type"
wrapperClassName={`${baseClass}__dropdown`}
/>
<InputField
{...fields.interval}
label="Interval Duration"
disabled={type.value !== 'interval'}
type="number"
/>
</div>
<div className={`${baseClass}__button-wrap`}>
<Button
className={`${baseClass}__form-btn ${baseClass}__form-btn--submit`}
type="submit"
onClick={handleSubmit}
>
Submit
</Button>
<Button
className={`${baseClass}__form-btn`}
type="inverse"
onClick={handleCancel}
>
Cancel
</Button>
</div>
</form>
);
}
}
export default Form(DecoratorForm, {
fields: ['id', 'name', 'type', 'query', 'interval', 'built_in'],
validate,
});

View File

@ -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(<DecoratorForm formData={formData} onTargetSelect={noop} handleSubmit={submitSpy} />);
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(<DecoratorForm formData={formData} onTargetSelect={noop} handleSubmit={submitSpy} />);
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(<DecoratorForm formData={formData} onTargetSelect={noop} onUpdate={updateSpy} />);
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(<DecoratorForm formData={formData} onTargetSelect={noop} onUpdate={updateSpy} />);
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)' },
});
});
});

View File

@ -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;
}
}
}

View File

@ -1 +0,0 @@
export default from './DecoratorForm';

View File

@ -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 (
<SecondarySidePanelContainer className={baseClass}>
<h3 className={`${baseClass}__title`}>
<Icon name="decorator" />
&nbsp;
What are Decorators?
</h3>
<p>
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.
</p>
<p>The types of decorators are:</p>
<ul>
<li>
<strong>load:</strong> run these decorators when the configuration loads (or is reloaded).
</li>
<li>
<strong>always:</strong> run these decorators before each query in the schedule.
</li>
<li>
<strong>interval:</strong> 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.
</li>
</ul>
<p>
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.
</p>
<p>
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.
</p>
</SecondarySidePanelContainer>
);
};
export default DecoratorInfoSidePanel;

View File

@ -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;
}
}
}

View File

@ -1 +0,0 @@
export default from './DecoratorInfoSidePanel';

View File

@ -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',

View File

@ -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',
};

View File

@ -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);
},
};
};

View File

@ -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);
});
});
});
});

View File

@ -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);
},
};
};

View File

@ -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);
});
});
});
});

View File

@ -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 = {}) {

View File

@ -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 (
<div className={`body-wrap ${baseClass}`}>
<div className={`${baseClass}__header-wrapper`}>
<div className={`${baseClass}__header-content`}>
<h1>Manage Additional Osquery Options</h1>
<p>
Osquery allows you to set a number of configuration options (<a href="https://osquery.io/docs/" target="_blank" rel="noopener noreferrer">Osquery Documentation</a>).
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.
</p>
</div>
<div className={`${baseClass}__btn-wrapper`}>
<Button block className={`${baseClass}__reset-btn`} onClick={onResetConfigOptions} variant="inverse">
RESET TO DEFAULT
</Button>
<Button block className={`${baseClass}__save-btn`} onClick={onSave} variant="brand">
SAVE OPTIONS
</Button>
</div>
</div>
<ConfigOptionsForm
configNameOptions={helpers.configOptionDropdownOptions(configOptions)}
completedOptions={availableOptions}
errors={configOptionErrors}
onFormUpdate={onOptionUpdate}
onRemoveOption={onRemoveOption}
/>
<Button onClick={onAddNewOption} variant="unstyled" className={`${baseClass}__add-new`}><Icon name="add-plus" /> Add New Option</Button>
</div>
);
}
}
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);

View File

@ -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(<ConfigOptionsPage {...loadingProps} />);
expect(page.html()).toNotExist();
});
it('renders when not loading the config', () => {
const page = mount(<ConfigOptionsPage {...props} />);
expect(page.length).toEqual(1);
});
it('renders reset and save buttons', () => {
const page = mount(<ConfigOptionsPage {...props} />);
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(<ConfigOptionsPage configOptions={[configOptionStub]} />);
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(<ConfigOptionsPage configOptions={[configOptionStub]} />);
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(<ConfigOptionsPage configOptions={[configOptionStub]} />);
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(<ConfigOptionsPage configOptions={[configOptionStub]} />);
const configOptionInput = page.find('ConfigOptionForm').find('InputField');
fillInFormInput(configOptionInput.find('input'), 'updated value');
expect(page.state('configOptions')).toEqual([
{ ...configOptionStub, value: 'updated value' },
]);
});
});
});

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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 };

View File

@ -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 },
]);
});
});
});

View File

@ -1 +0,0 @@
export default from './ConfigOptionsPage';

View File

@ -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 (
<div className={`${baseClass} has-sidebar`}>
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__form body-wrap`}>
<DecoratorForm
formData={decorator}
handleSubmit={onSubmit}
handleCancel={onCancel}
newDecorator={newDecorator}
/>
</div>
</div>
<QuerySidePanel
onOsqueryTableSelect={onOsqueryTableSelect}
onTextEditorInputChange={noop}
selectedOsqueryTable={selectedOsqueryTable}
/>
</div>
);
}
}
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);

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export default from './DecoratorPage';

View File

@ -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 (
<Modal
title="Delete Decorator"
onExit={toggleDeleteModal}
>
<p>Are you sure that you want to delete the selected decorators?</p>
<div className={`${baseClass}__modal-btn-wrap`}>
<Button onClick={onDeleteDecorators} variant="alert">Delete</Button>
<Button onClick={toggleDeleteModal} variant="inverse">Cancel</Button>
</div>
</Modal>
);
}
renderNewButton = () => {
return (
<Button
variant="brand"
onClick={this.showNewQueryPage}
>
CREATE DECORATOR
</Button>
);
}
renderDeleteButton = () => {
return (
<div>
<Button
onClick={this.toggleDeleteModal}
variant="alert"
>
Delete
</Button>
</div>
);
}
renderEditButton = () => {
return (
<div>
<Button
onClick={this.showEditDecorator}
>
EDIT DECORATOR
</Button>
</div>
);
}
renderSidePanel = () => {
return (
<DecoratorInfoSidePanel />
);
}
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 (
<div className={`${baseClass} has-sidebar`}>
<div className={`${baseClass}__wrapper body-wrap`}>
<h1 className={`${baseClass}__title`}>
<NumberPill number={decorators.length} /> Osquery Decorators
<div className={`${baseClass}__top-buttons`}>
{this.renderButtons()}
</div>
</h1>
<DecoratorRows
decorators={decorators}
onCheckDecorator={onCheckDecorator}
onCheckAll={onCheckAll}
allChecked={allChecked}
checkedDecoratorIDs={checkedIDs}
onSelectDecorator={onSelectDecorator}
onDoubleClick={onDoubleClick}
selectedDecorator={selectedDecorator}
/>
</div>
{renderSidePanel()}
{renderDeleteConfirmationModel()}
</div>
);
}
}
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);

View File

@ -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;
}
}
}

View File

@ -1 +0,0 @@
export default from './ManageDecoratorsPage';

View File

@ -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 },
};
};

View File

@ -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;

View File

@ -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,
});

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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);
});
});
});
});

View File

@ -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,
});

View File

@ -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 });
}
};

View File

@ -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,
});
});
});
});

View File

@ -1,3 +0,0 @@
import config from './config';
export default config.actions;

View File

@ -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,
});

View File

@ -1,3 +0,0 @@
import config from './config';
export default config.reducer;

View File

@ -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,
});

View File

@ -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 = (
<Route path="users" component={AdminUserManagementPage} />
<Route path="settings" component={AdminAppSettingsPage} />
</Route>
<Route path="config">
<Route path="options" component={ConfigOptionsPage} />
</Route>
<Route path="hosts">
<Route path="manage" component={ManageHostsPage} />
</Route>
<Route path="decorators" component={DecoratorsPageWrapper}>
<Route path="manage" component={ManageDecoratorsPage} />
<Route path="new" component={DecoratorPage} />
<Route path=":id" component={DecoratorPage} />
</Route>
<Route path="packs" component={PackPageWrapper}>
<Route path="manage" component={AllPacksPage} />
<Route path="new" component={PackComposerPage} />

View File

@ -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',

View File

@ -376,10 +376,6 @@
content: '\f070';
}
.kolidecon-decorator:before {
content: '\f073';
}
.sr-only {
position: absolute;
width: 1px;

View File

@ -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: [] },
});
},
},
};

View File

@ -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: {},
});
},
},
};

View File

@ -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,

View File

@ -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,
};

View File

@ -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{})

View File

@ -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)
}

Some files were not shown because too many files have changed in this diff Show More