mirror of
https://github.com/empayre/fleet.git
synced 2024-11-07 01:15:22 +00:00
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:
commit
c273a92537
@ -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" }}
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,3 +1,40 @@
|
||||
## Kolide Fleet 2.0.0 (currently preparing for release)
|
||||
|
||||
The primary new addition in Fleet 2 is the new `fleetctl` CLI and file-format, which dramatically increases the flexibility and control that administrators have over their osquery deployment. The CLI and the file format are documented [in the Fleet documentation](https://github.com/kolide/fleet/blob/master/docs/cli/README.md).
|
||||
|
||||
### New Features
|
||||
|
||||
* New `fleetctl` CLI for managing your entire osquery workflow via CLI, API, and source controlled files!
|
||||
* You can use `fleetctl` to manage osquery packs, queries, labels, and configuration.
|
||||
|
||||
* In addition to the CLI, Fleet 2.0.0 introduces a new file format for articulating labels, queries, packs, options, etc. This format is designed for composability, enabling more effective sharing and re-use of intelligence.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: pending_updates
|
||||
query: >
|
||||
select value
|
||||
from plist
|
||||
where
|
||||
path = "/Library/Preferences/ManagedInstalls.plist" and
|
||||
key = "PendingUpdateCount" and
|
||||
value > "0";
|
||||
```
|
||||
|
||||
* Run live osquery queries against arbitrary subsets of your infrastructure via the `fleetctl query` command.
|
||||
|
||||
* Use `fleetctl setup`, `fleetctl login`, and `fleetctl logout` to manage the authentication life-cycle via the CLI.
|
||||
|
||||
* Use `fleetctl get`, `fleetctl apply`, and `fleetctl delete` to manage the state of your Fleet data.
|
||||
|
||||
* Manage any osquery option you want and set platform-specific overrides with the `fleetctl` CLI and file format.
|
||||
|
||||
### Upgrade Plan
|
||||
|
||||
* Managing osquery options via the UI has been removed in favor of the more flexible solution provided by the CLI. If you have customized your osquery options with Fleet, there is [a database migration](server/datastore/mysql/migrations/data/20171212182458_MigrateOsqueryOptions.go) which will port your existing data into the new format when you run `fleet prepare db`. To download your osquery options after migrating your database, run `fleetctl get options > options.yaml`. Further modifications to your options should occur in this file and it should be applied with `fleetctl apply -f ./options.yaml`.
|
||||
|
||||
## Kolide Fleet 1.0.8 (May 3, 2018)
|
||||
|
||||
* Osquery 3.0+ compatibility!
|
||||
|
145
Gopkg.lock
generated
145
Gopkg.lock
generated
@ -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
|
||||
|
@ -44,7 +44,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/igm/sockjs-go"
|
||||
version = "2.0.0"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
|
2
Makefile
2
Makefile
@ -100,7 +100,7 @@ endif
|
||||
.pre-fleetctl:
|
||||
$(eval APP_NAME = fleetctl)
|
||||
|
||||
build: fleet
|
||||
build: fleet fleetctl
|
||||
|
||||
fleet: .prefix .pre-build .pre-fleet
|
||||
go build -i -o build/${OUTPUT} -ldflags ${KIT_VERSION} ./cmd/fleet
|
||||
|
18
README.md
18
README.md
@ -10,13 +10,9 @@ Documentation for Fleet can be found on [GitHub](./docs/README.md).
|
||||
|
||||
## Using Fleet
|
||||
|
||||
#### The Web UI
|
||||
|
||||
Information about using the Kolide web application can be found in the [Application Documentation](./docs/application/README.md).
|
||||
|
||||
#### The CLI
|
||||
|
||||
If you're interested in learning about the (under development) `fleetctl` CLI and flexible osquery deployment file format, see the [CLI Documentation](./docs/cli/README.md).
|
||||
If you're interested in learning about the `fleetctl` CLI and flexible osquery deployment file format, see the [CLI Documentation](./docs/cli/README.md).
|
||||
|
||||
#### Deploying Osquery and Fleet
|
||||
|
||||
@ -26,6 +22,10 @@ Resources for deploying osquery to hosts, deploying the Fleet server, installing
|
||||
|
||||
If you are interested in accessing the Fleet REST API in order to programmatically interact with your osquery installation, please see the [API Documentation](./docs/api/README.md).
|
||||
|
||||
#### The Web Dashboard
|
||||
|
||||
Information about using the Kolide web dashboard can be found in the [Dashboard Documentation](./docs/dashboard/README.md).
|
||||
|
||||
## Developing Fleet
|
||||
|
||||
#### Development Documentation
|
||||
@ -38,7 +38,7 @@ If you have any questions, please create a [GitHub Issue](https://github.com/kol
|
||||
|
||||
#### Chat
|
||||
|
||||
Please join us in the #kolide channel on [osquery Slack](https://osquery-slack.herokuapp.com/).
|
||||
Please join us in the #kolide channel on [Osquery Slack](https://osquery-slack.herokuapp.com/).
|
||||
|
||||
#### Community Projects
|
||||
|
||||
@ -46,3 +46,9 @@ Below are some projects created by Kolide community members. Please submit a pul
|
||||
|
||||
- [davidrecordon/terraform-aws-kolide-fleet](https://github.com/davidrecordon/terraform-aws-kolide-fleet) - Deploy Fleet into AWS using Terraform.
|
||||
- [deeso/fleet-deployment](https://github.com/deeso/fleet-deployment) - Install Fleet on a Ubuntu box.
|
||||
|
||||
## Kolide Cloud
|
||||
|
||||
Looking for the quickest way to try out osquery on your fleet? Not sure which queries to run? Don't want to manage your own data pipeline?
|
||||
|
||||
Try our [osquery SaaS platform](https://kolide.com/?utm_source=oss&utm_medium=readme&utm_campaign=fleet) providing insights, alerting, fleet management and user-driven security tools. We also support advanced aggregation of osquery results for power users. Get started immediately, and your first 10 hosts are free.
|
||||
|
47
cmd/fleetctl/api.go
Normal file
47
cmd/fleetctl/api.go
Normal 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
168
cmd/fleetctl/apply.go
Normal 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
296
cmd/fleetctl/config.go
Normal 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
165
cmd/fleetctl/convert.go
Normal 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
97
cmd/fleetctl/delete.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
@ -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
270
cmd/fleetctl/get.go
Normal 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
93
cmd/fleetctl/login.go
Normal 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
38
cmd/fleetctl/logout.go
Normal 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
131
cmd/fleetctl/query.go
Normal 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
80
cmd/fleetctl/setup.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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.
|
@ -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.
|
@ -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).
|
||||
|
@ -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
9
docs/dashboard/README.md
Normal 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).
|
@ -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
22
docs/development/faq.md
Normal 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.
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorRow';
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorRows';
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorsPageWrapper';
|
@ -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 });
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -1 +0,0 @@
|
||||
export default from './ConfigOptionForm';
|
@ -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;
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
@ -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%;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './ConfigOptionsForm';
|
@ -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,
|
||||
});
|
@ -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)' },
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorForm';
|
@ -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" />
|
||||
|
||||
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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorInfoSidePanel';
|
@ -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',
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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 = {}) {
|
||||
|
@ -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);
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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 };
|
@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +0,0 @@
|
||||
export default from './ConfigOptionsPage';
|
@ -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);
|
@ -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;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './DecoratorPage';
|
@ -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);
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './ManageDecoratorsPage';
|
@ -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 },
|
||||
};
|
||||
};
|
@ -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;
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
import config from './config';
|
||||
|
||||
export default config.actions;
|
@ -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,
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
import config from './config';
|
||||
|
||||
export default config.reducer;
|
@ -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,
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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',
|
||||
|
@ -376,10 +376,6 @@
|
||||
content: '\f070';
|
||||
}
|
||||
|
||||
.kolidecon-decorator:before {
|
||||
content: '\f073';
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
@ -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: [] },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
@ -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: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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{})
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user