2016-09-26 18:48:55 +00:00
|
|
|
package service
|
2016-09-04 05:13:42 +00:00
|
|
|
|
|
|
|
import (
|
2017-03-15 15:55:30 +00:00
|
|
|
"context"
|
2016-09-04 05:13:42 +00:00
|
|
|
"encoding/json"
|
2016-10-05 00:17:55 +00:00
|
|
|
"fmt"
|
2020-03-11 20:38:44 +00:00
|
|
|
"net"
|
2016-10-05 00:17:55 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2016-09-04 05:13:42 +00:00
|
|
|
|
2017-01-19 23:28:33 +00:00
|
|
|
"github.com/go-kit/kit/log"
|
2017-06-22 19:50:45 +00:00
|
|
|
hostctx "github.com/kolide/fleet/server/contexts/host"
|
|
|
|
"github.com/kolide/fleet/server/kolide"
|
|
|
|
"github.com/kolide/fleet/server/pubsub"
|
2017-01-17 06:03:51 +00:00
|
|
|
"github.com/pkg/errors"
|
2017-04-06 18:55:24 +00:00
|
|
|
"github.com/spf13/cast"
|
2016-09-04 05:13:42 +00:00
|
|
|
)
|
|
|
|
|
2016-09-21 03:08:11 +00:00
|
|
|
type osqueryError struct {
|
2016-09-29 04:21:39 +00:00
|
|
|
message string
|
|
|
|
nodeInvalid bool
|
2016-09-21 03:08:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e osqueryError) Error() string {
|
|
|
|
return e.message
|
|
|
|
}
|
|
|
|
|
2016-09-29 04:21:39 +00:00
|
|
|
func (e osqueryError) NodeInvalid() bool {
|
|
|
|
return e.nodeInvalid
|
|
|
|
}
|
|
|
|
|
2017-06-08 17:57:12 +00:00
|
|
|
// Sometimes osquery gives us empty string where we expect an integer.
|
|
|
|
// We change the to "0" so it can be handled by the appropriate string to
|
|
|
|
// integer conversion function, as these will err on ""
|
|
|
|
func emptyToZero(val string) string {
|
|
|
|
if val == "" {
|
|
|
|
return "0"
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
2016-09-29 04:21:39 +00:00
|
|
|
func (svc service) AuthenticateHost(ctx context.Context, nodeKey string) (*kolide.Host, error) {
|
|
|
|
if nodeKey == "" {
|
|
|
|
return nil, osqueryError{
|
|
|
|
message: "authentication error: missing node key",
|
|
|
|
nodeInvalid: true,
|
|
|
|
}
|
|
|
|
}
|
2017-03-30 15:31:05 +00:00
|
|
|
|
2016-09-29 04:21:39 +00:00
|
|
|
host, err := svc.ds.AuthenticateHost(nodeKey)
|
|
|
|
if err != nil {
|
2017-05-25 21:10:12 +00:00
|
|
|
switch err.(type) {
|
|
|
|
case kolide.NotFoundError:
|
|
|
|
return nil, osqueryError{
|
|
|
|
message: "authentication error: invalid node key: " + nodeKey,
|
|
|
|
nodeInvalid: true,
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, osqueryError{
|
|
|
|
message: "authentication error: " + err.Error(),
|
|
|
|
}
|
2016-09-29 04:21:39 +00:00
|
|
|
}
|
|
|
|
}
|
2017-03-30 15:31:05 +00:00
|
|
|
|
|
|
|
// Update the "seen" time used to calculate online status
|
2017-01-11 16:30:13 +00:00
|
|
|
err = svc.ds.MarkHostSeen(host, svc.clock.Now())
|
|
|
|
if err != nil {
|
2017-03-30 15:31:05 +00:00
|
|
|
return nil, osqueryError{message: "failed to mark host seen: " + err.Error()}
|
2017-01-11 16:30:13 +00:00
|
|
|
}
|
2017-03-30 15:31:05 +00:00
|
|
|
|
2016-09-29 04:21:39 +00:00
|
|
|
return host, nil
|
|
|
|
}
|
|
|
|
|
2019-07-01 23:50:04 +00:00
|
|
|
func (svc service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string, hostDetails map[string](map[string]string)) (string, error) {
|
2020-05-29 16:12:39 +00:00
|
|
|
secretName, err := svc.ds.VerifyEnrollSecret(enrollSecret)
|
2017-01-20 19:48:54 +00:00
|
|
|
if err != nil {
|
2020-05-29 16:12:39 +00:00
|
|
|
return "", osqueryError{
|
|
|
|
message: "enroll failed: " + err.Error(),
|
|
|
|
nodeInvalid: true,
|
|
|
|
}
|
2017-01-20 19:48:54 +00:00
|
|
|
}
|
|
|
|
|
2020-05-29 16:12:39 +00:00
|
|
|
nodeKey, err := kolide.RandomText(svc.config.Osquery.NodeKeySize)
|
|
|
|
if err != nil {
|
|
|
|
return "", osqueryError{
|
|
|
|
message: "generate node key failed: " + err.Error(),
|
|
|
|
nodeInvalid: true,
|
|
|
|
}
|
2016-09-04 05:13:42 +00:00
|
|
|
}
|
|
|
|
|
2020-05-29 16:12:39 +00:00
|
|
|
host, err := svc.ds.EnrollHost(hostIdentifier, nodeKey, secretName)
|
2016-09-04 05:13:42 +00:00
|
|
|
if err != nil {
|
2020-05-29 16:12:39 +00:00
|
|
|
return "", osqueryError{message: "save enroll failed: " + err.Error(), nodeInvalid: true}
|
2016-09-04 05:13:42 +00:00
|
|
|
}
|
|
|
|
|
2019-07-01 23:50:04 +00:00
|
|
|
// Save enrollment details if provided
|
|
|
|
save := false
|
|
|
|
if r, ok := hostDetails["os_version"]; ok {
|
|
|
|
detailQueries["os_version"].IngestFunc(svc.logger, host, []map[string]string{r})
|
|
|
|
save = true
|
|
|
|
}
|
|
|
|
if r, ok := hostDetails["osquery_info"]; ok {
|
|
|
|
detailQueries["osquery_info"].IngestFunc(svc.logger, host, []map[string]string{r})
|
|
|
|
save = true
|
|
|
|
}
|
|
|
|
if r, ok := hostDetails["system_info"]; ok {
|
|
|
|
detailQueries["system_info"].IngestFunc(svc.logger, host, []map[string]string{r})
|
|
|
|
save = true
|
|
|
|
}
|
|
|
|
if save {
|
|
|
|
if err := svc.ds.SaveHost(host); err != nil {
|
|
|
|
return "", osqueryError{message: "saving host details: " + err.Error(), nodeInvalid: true}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-04 05:13:42 +00:00
|
|
|
return host.NodeKey, nil
|
|
|
|
}
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
func (svc service) GetClientConfig(ctx context.Context) (map[string]interface{}, error) {
|
2016-10-03 03:14:35 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, osqueryError{message: "internal error: missing host from request context"}
|
|
|
|
}
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
baseConfig, err := svc.ds.OptionsForPlatform(host.Platform)
|
|
|
|
if err != nil {
|
|
|
|
return nil, osqueryError{message: "internal error: fetching base config: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
var config map[string]interface{}
|
|
|
|
err = json.Unmarshal(baseConfig, &config)
|
2016-12-31 17:56:54 +00:00
|
|
|
if err != nil {
|
2017-12-13 23:14:54 +00:00
|
|
|
return nil, osqueryError{message: "internal error: parsing base configuration: " + err.Error()}
|
2016-12-31 17:56:54 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 19:38:20 +00:00
|
|
|
packs, err := svc.ds.ListPacksForHost(host.ID)
|
2016-10-03 03:14:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, osqueryError{message: "database error: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
packConfig := kolide.Packs{}
|
2016-10-03 03:14:35 +00:00
|
|
|
for _, pack := range packs {
|
|
|
|
// first, we must figure out what queries are in this pack
|
2016-12-13 22:22:05 +00:00
|
|
|
queries, err := svc.ds.ListScheduledQueriesInPack(pack.ID, kolide.ListOptions{})
|
2016-10-03 03:14:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, osqueryError{message: "database error: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
// the serializable osquery config struct expects content in a
|
|
|
|
// particular format, so we do the conversion here
|
|
|
|
configQueries := kolide.Queries{}
|
|
|
|
for _, query := range queries {
|
2017-01-10 22:27:52 +00:00
|
|
|
queryContent := kolide.QueryContent{
|
2016-10-03 03:14:35 +00:00
|
|
|
Query: query.Query,
|
|
|
|
Interval: query.Interval,
|
|
|
|
Platform: query.Platform,
|
|
|
|
Version: query.Version,
|
2018-01-03 00:06:50 +00:00
|
|
|
Removed: query.Removed,
|
|
|
|
Shard: query.Shard,
|
|
|
|
}
|
|
|
|
|
|
|
|
if query.Removed != nil {
|
|
|
|
queryContent.Removed = query.Removed
|
2016-10-03 03:14:35 +00:00
|
|
|
}
|
2017-01-10 22:27:52 +00:00
|
|
|
|
2017-12-04 14:43:43 +00:00
|
|
|
if query.Snapshot != nil && *query.Snapshot {
|
2017-01-10 22:27:52 +00:00
|
|
|
queryContent.Snapshot = query.Snapshot
|
|
|
|
}
|
|
|
|
|
|
|
|
configQueries[query.Name] = queryContent
|
2016-10-03 03:14:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// finally, we add the pack to the client config struct with all of
|
2017-12-13 23:14:54 +00:00
|
|
|
// the pack's queries
|
|
|
|
packConfig[pack.Name] = kolide.PackContent{
|
2016-10-03 03:14:35 +00:00
|
|
|
Platform: pack.Platform,
|
|
|
|
Queries: configQueries,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
if len(packConfig) > 0 {
|
|
|
|
packJSON, err := json.Marshal(packConfig)
|
|
|
|
if err != nil {
|
|
|
|
return nil, osqueryError{message: "internal error: marshal pack JSON: " + err.Error()}
|
|
|
|
}
|
|
|
|
config["packs"] = json.RawMessage(packJSON)
|
|
|
|
}
|
|
|
|
|
2017-04-06 18:55:24 +00:00
|
|
|
// Save interval values if they have been updated. Note
|
|
|
|
// config_tls_refresh can only be set in the osquery flags so is
|
|
|
|
// ignored here.
|
|
|
|
saveHost := false
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
if options, ok := config["options"].(map[string]interface{}); ok {
|
|
|
|
distributedIntervalVal, ok := options["distributed_interval"]
|
|
|
|
distributedInterval, err := cast.ToUintE(distributedIntervalVal)
|
|
|
|
if ok && err == nil && host.DistributedInterval != distributedInterval {
|
|
|
|
host.DistributedInterval = distributedInterval
|
|
|
|
saveHost = true
|
|
|
|
}
|
2017-04-06 18:55:24 +00:00
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
loggerTLSPeriodVal, ok := options["logger_tls_period"]
|
|
|
|
loggerTLSPeriod, err := cast.ToUintE(loggerTLSPeriodVal)
|
|
|
|
if ok && err == nil && host.LoggerTLSPeriod != loggerTLSPeriod {
|
|
|
|
host.LoggerTLSPeriod = loggerTLSPeriod
|
|
|
|
saveHost = true
|
|
|
|
}
|
2017-04-06 18:55:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if saveHost {
|
|
|
|
err := svc.ds.SaveHost(&host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-13 23:14:54 +00:00
|
|
|
return config, nil
|
2016-09-04 05:13:42 +00:00
|
|
|
}
|
|
|
|
|
2017-12-12 15:43:33 +00:00
|
|
|
func (svc service) SubmitStatusLogs(ctx context.Context, logs []json.RawMessage) error {
|
2019-07-16 22:41:50 +00:00
|
|
|
if err := svc.osqueryLogWriter.Status.Write(ctx, logs); err != nil {
|
2019-04-08 18:47:15 +00:00
|
|
|
return osqueryError{message: "error writing status logs: " + err.Error()}
|
2017-04-03 21:48:50 +00:00
|
|
|
}
|
2016-09-04 05:13:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-11-19 00:59:32 +00:00
|
|
|
func (svc service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage) error {
|
2019-07-16 22:41:50 +00:00
|
|
|
if err := svc.osqueryLogWriter.Result.Write(ctx, logs); err != nil {
|
2019-04-08 18:47:15 +00:00
|
|
|
return osqueryError{message: "error writing result logs: " + err.Error()}
|
2017-04-03 21:48:50 +00:00
|
|
|
}
|
2016-09-29 04:21:39 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-21 03:08:11 +00:00
|
|
|
// hostLabelQueryPrefix is appended before the query name when a query is
|
|
|
|
// provided as a label query. This allows the results to be retrieved when
|
|
|
|
// osqueryd writes the distributed query results.
|
|
|
|
const hostLabelQueryPrefix = "kolide_label_query_"
|
|
|
|
|
|
|
|
// hostDetailQueryPrefix is appended before the query name when a query is
|
|
|
|
// provided as a detail query.
|
|
|
|
const hostDetailQueryPrefix = "kolide_detail_query_"
|
|
|
|
|
2020-05-21 15:36:00 +00:00
|
|
|
// hostAdditionalQueryPrefix is appended before the query name when a query is
|
|
|
|
// provided as an additional query (additional info for hosts to retrieve).
|
|
|
|
const hostAdditionalQueryPrefix = "kolide_additional_query_"
|
|
|
|
|
2016-11-14 18:22:54 +00:00
|
|
|
// hostDistributedQueryPrefix is appended before the query name when a query is
|
|
|
|
// run from a distributed query campaign
|
|
|
|
const hostDistributedQueryPrefix = "kolide_distributed_query_"
|
|
|
|
|
2016-10-05 00:17:55 +00:00
|
|
|
// detailQueries defines the detail queries that should be run on the host, as
|
|
|
|
// well as how the results of those queries should be ingested into the
|
|
|
|
// kolide.Host data model. This map should not be modified at runtime.
|
|
|
|
var detailQueries = map[string]struct {
|
|
|
|
Query string
|
2017-01-19 23:28:33 +00:00
|
|
|
IngestFunc func(logger log.Logger, host *kolide.Host, rows []map[string]string) error
|
2016-10-05 00:17:55 +00:00
|
|
|
}{
|
2017-04-06 18:55:24 +00:00
|
|
|
"network_interface": {
|
2020-03-11 20:38:44 +00:00
|
|
|
Query: `select address, mac
|
2018-02-05 05:16:24 +00:00
|
|
|
from interface_details id join interface_addresses ia
|
|
|
|
on ia.interface = id.interface where length(mac) > 0
|
|
|
|
order by (ibytes + obytes) desc`,
|
2017-04-06 18:55:24 +00:00
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) (err error) {
|
|
|
|
if len(rows) == 0 {
|
2017-01-19 23:28:33 +00:00
|
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
2017-04-06 18:55:24 +00:00
|
|
|
"detail_query_network_interface expected 1 or more results")
|
2017-01-19 23:28:33 +00:00
|
|
|
return nil
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
2017-04-06 18:55:24 +00:00
|
|
|
for _, row := range rows {
|
2020-03-11 20:38:44 +00:00
|
|
|
ip := net.ParseIP(row["address"])
|
|
|
|
if ip == nil {
|
|
|
|
continue
|
2017-04-06 18:55:24 +00:00
|
|
|
}
|
2020-03-11 20:38:44 +00:00
|
|
|
|
|
|
|
// Skip link-local and loopback interfaces
|
|
|
|
if ip.IsLinkLocalUnicast() || ip.IsLoopback() {
|
|
|
|
continue
|
2017-04-06 18:55:24 +00:00
|
|
|
}
|
2016-10-05 00:17:55 +00:00
|
|
|
|
2020-03-11 20:38:44 +00:00
|
|
|
// Rows are ordered by traffic, so we will get the most active
|
|
|
|
// interface by iterating in order
|
|
|
|
host.PrimaryIP = row["address"]
|
|
|
|
host.PrimaryMac = row["mac"]
|
|
|
|
return nil
|
|
|
|
}
|
2017-04-06 18:55:24 +00:00
|
|
|
|
2020-03-11 20:38:44 +00:00
|
|
|
// If only link-local and loopback found, still use the first
|
|
|
|
// interface.
|
|
|
|
host.PrimaryIP = rows[0]["address"]
|
|
|
|
host.PrimaryMac = rows[0]["mac"]
|
2016-10-05 00:17:55 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"os_version": {
|
|
|
|
Query: "select * from os_version limit 1",
|
2017-01-19 23:28:33 +00:00
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error {
|
2016-10-05 00:17:55 +00:00
|
|
|
if len(rows) != 1 {
|
2017-01-19 23:28:33 +00:00
|
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
|
|
fmt.Sprintf("detail_query_os_version expected single result got %d", len(rows)))
|
|
|
|
return nil
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
host.OSVersion = fmt.Sprintf(
|
|
|
|
"%s %s.%s.%s",
|
|
|
|
rows[0]["name"],
|
|
|
|
rows[0]["major"],
|
|
|
|
rows[0]["minor"],
|
|
|
|
rows[0]["patch"],
|
|
|
|
)
|
2017-01-20 18:18:39 +00:00
|
|
|
host.OSVersion = strings.Trim(host.OSVersion, ".")
|
2016-10-05 00:17:55 +00:00
|
|
|
|
2016-12-01 17:00:00 +00:00
|
|
|
if build, ok := rows[0]["build"]; ok {
|
|
|
|
host.Build = build
|
|
|
|
}
|
|
|
|
|
2017-02-22 03:22:34 +00:00
|
|
|
host.Platform = rows[0]["platform"]
|
2016-12-01 17:00:00 +00:00
|
|
|
host.PlatformLike = rows[0]["platform_like"]
|
|
|
|
host.CodeName = rows[0]["code_name"]
|
2017-03-15 22:40:18 +00:00
|
|
|
|
|
|
|
// On centos6 there is an osquery bug that leaves
|
|
|
|
// platform empty. Here we workaround.
|
|
|
|
if host.Platform == "" &&
|
|
|
|
strings.Contains(strings.ToLower(rows[0]["name"]), "centos") {
|
|
|
|
host.Platform = "centos"
|
|
|
|
}
|
|
|
|
|
2016-10-05 00:17:55 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
2017-04-06 18:55:24 +00:00
|
|
|
"osquery_flags": {
|
|
|
|
// Collect the interval info (used for online status
|
|
|
|
// calculation) from the osquery flags. We typically control
|
|
|
|
// distributed_interval (but it's not required), and typically
|
|
|
|
// do not control config_tls_refresh.
|
2017-06-20 00:04:21 +00:00
|
|
|
Query: `select name, value from osquery_flags where name in ("distributed_interval", "config_tls_refresh", "config_refresh", "logger_tls_period")`,
|
2017-04-06 18:55:24 +00:00
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error {
|
2017-06-20 00:04:21 +00:00
|
|
|
var configTLSRefresh, configRefresh uint
|
2017-04-06 18:55:24 +00:00
|
|
|
for _, row := range rows {
|
|
|
|
switch row["name"] {
|
|
|
|
|
|
|
|
case "distributed_interval":
|
2017-06-08 17:57:12 +00:00
|
|
|
interval, err := strconv.Atoi(emptyToZero(row["value"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "parsing distributed_interval")
|
|
|
|
}
|
|
|
|
host.DistributedInterval = uint(interval)
|
|
|
|
|
|
|
|
case "config_tls_refresh":
|
2017-06-20 00:04:21 +00:00
|
|
|
// Prior to osquery 2.4.6, the flag was
|
|
|
|
// called `config_tls_refresh`.
|
2017-06-08 17:57:12 +00:00
|
|
|
interval, err := strconv.Atoi(emptyToZero(row["value"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "parsing config_tls_refresh")
|
|
|
|
}
|
2017-06-20 00:04:21 +00:00
|
|
|
configTLSRefresh = uint(interval)
|
|
|
|
|
|
|
|
case "config_refresh":
|
|
|
|
// After 2.4.6 `config_tls_refresh` was
|
|
|
|
// aliased to `config_refresh`.
|
|
|
|
interval, err := strconv.Atoi(emptyToZero(row["value"]))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "parsing config_refresh")
|
|
|
|
}
|
|
|
|
configRefresh = uint(interval)
|
2017-04-06 18:55:24 +00:00
|
|
|
|
|
|
|
case "logger_tls_period":
|
2017-06-08 17:57:12 +00:00
|
|
|
interval, err := strconv.Atoi(emptyToZero(row["value"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "parsing logger_tls_period")
|
|
|
|
}
|
|
|
|
host.LoggerTLSPeriod = uint(interval)
|
|
|
|
}
|
|
|
|
}
|
2017-06-20 00:04:21 +00:00
|
|
|
|
|
|
|
// Since the `config_refresh` flag existed prior to
|
|
|
|
// 2.4.6 and had a different meaning, we prefer
|
|
|
|
// `config_tls_refresh` if it was set, and use
|
|
|
|
// `config_refresh` as a fallback.
|
|
|
|
if configTLSRefresh != 0 {
|
|
|
|
host.ConfigTLSRefresh = configTLSRefresh
|
|
|
|
} else {
|
|
|
|
host.ConfigTLSRefresh = configRefresh
|
|
|
|
}
|
|
|
|
|
2017-04-06 18:55:24 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"osquery_info": {
|
|
|
|
Query: "select * from osquery_info limit 1",
|
2017-01-19 23:28:33 +00:00
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error {
|
2016-10-05 00:17:55 +00:00
|
|
|
if len(rows) != 1 {
|
2017-01-19 23:28:33 +00:00
|
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
2017-04-06 18:55:24 +00:00
|
|
|
fmt.Sprintf("detail_query_osquery_info expected single result got %d", len(rows)))
|
2017-01-19 23:28:33 +00:00
|
|
|
return nil
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
2017-04-06 18:55:24 +00:00
|
|
|
host.OsqueryVersion = rows[0]["version"]
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"system_info": {
|
|
|
|
Query: "select * from system_info limit 1",
|
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error {
|
|
|
|
if len(rows) != 1 {
|
|
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
|
|
fmt.Sprintf("detail_query_system_info expected single result got %d", len(rows)))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2017-06-08 17:57:12 +00:00
|
|
|
host.PhysicalMemory, err = strconv.Atoi(emptyToZero(rows[0]["physical_memory"]))
|
2016-10-05 00:17:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-04-06 18:55:24 +00:00
|
|
|
host.HostName = rows[0]["hostname"]
|
|
|
|
host.UUID = rows[0]["uuid"]
|
|
|
|
host.CPUType = rows[0]["cpu_type"]
|
|
|
|
host.CPUSubtype = rows[0]["cpu_subtype"]
|
|
|
|
host.CPUBrand = rows[0]["cpu_brand"]
|
2017-06-08 17:57:12 +00:00
|
|
|
host.CPUPhysicalCores, err = strconv.Atoi(emptyToZero(rows[0]["cpu_physical_cores"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-06-08 17:57:12 +00:00
|
|
|
host.CPULogicalCores, err = strconv.Atoi(emptyToZero(rows[0]["cpu_logical_cores"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
host.HardwareVendor = rows[0]["hardware_vendor"]
|
|
|
|
host.HardwareModel = rows[0]["hardware_model"]
|
|
|
|
host.HardwareVersion = rows[0]["hardware_version"]
|
|
|
|
host.HardwareSerial = rows[0]["hardware_serial"]
|
|
|
|
host.ComputerName = rows[0]["computer_name"]
|
2016-10-05 00:17:55 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
2017-04-06 18:55:24 +00:00
|
|
|
"uptime": {
|
|
|
|
Query: "select * from uptime limit 1",
|
|
|
|
IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error {
|
|
|
|
if len(rows) != 1 {
|
2017-01-19 23:28:33 +00:00
|
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
2017-04-06 18:55:24 +00:00
|
|
|
fmt.Sprintf("detail_query_uptime expected single result got %d", len(rows)))
|
2017-01-19 23:28:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
2016-12-01 17:00:00 +00:00
|
|
|
|
2017-06-08 17:57:12 +00:00
|
|
|
uptimeSeconds, err := strconv.Atoi(emptyToZero(rows[0]["total_seconds"]))
|
2017-04-06 18:55:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
2017-04-06 18:55:24 +00:00
|
|
|
host.Uptime = time.Duration(uptimeSeconds) * time.Second
|
2016-10-05 00:17:55 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2016-09-21 03:08:11 +00:00
|
|
|
// hostDetailQueries returns the map of queries that should be executed by
|
|
|
|
// osqueryd to fill in the host details
|
2020-05-21 15:36:00 +00:00
|
|
|
func (svc service) hostDetailQueries(host kolide.Host) (map[string]string, error) {
|
2016-09-21 03:08:11 +00:00
|
|
|
queries := make(map[string]string)
|
2020-03-02 19:08:08 +00:00
|
|
|
if host.DetailUpdateTime.After(svc.clock.Now().Add(-svc.config.Osquery.DetailUpdateInterval)) {
|
2016-10-05 00:17:55 +00:00
|
|
|
// No need to update already fresh details
|
2020-05-21 15:36:00 +00:00
|
|
|
return queries, nil
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for name, query := range detailQueries {
|
|
|
|
queries[hostDetailQueryPrefix+name] = query.Query
|
2016-09-21 03:08:11 +00:00
|
|
|
}
|
2020-05-21 15:36:00 +00:00
|
|
|
|
|
|
|
// Get additional queries
|
|
|
|
config, err := svc.ds.AppConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, osqueryError{message: "get additional queries: " + err.Error()}
|
|
|
|
}
|
|
|
|
if config.AdditionalQueries == nil {
|
|
|
|
// No additional queries set
|
|
|
|
return queries, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var additionalQueries map[string]string
|
|
|
|
if err := json.Unmarshal(*config.AdditionalQueries, &additionalQueries); err != nil {
|
|
|
|
return nil, osqueryError{message: "unmarshal additional queries: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, query := range additionalQueries {
|
|
|
|
queries[hostAdditionalQueryPrefix+name] = query
|
|
|
|
}
|
|
|
|
|
|
|
|
return queries, nil
|
2016-09-21 03:08:11 +00:00
|
|
|
}
|
|
|
|
|
2017-03-21 16:17:38 +00:00
|
|
|
func (svc service) GetDistributedQueries(ctx context.Context) (map[string]string, uint, error) {
|
2016-09-29 04:21:39 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
2016-09-26 17:14:39 +00:00
|
|
|
if !ok {
|
2017-03-21 16:17:38 +00:00
|
|
|
return nil, 0, osqueryError{message: "internal error: missing host from request context"}
|
2016-09-21 03:08:11 +00:00
|
|
|
}
|
|
|
|
|
2020-05-21 15:36:00 +00:00
|
|
|
queries, err := svc.hostDetailQueries(host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2016-09-21 03:08:11 +00:00
|
|
|
|
|
|
|
// Retrieve the label queries that should be updated
|
|
|
|
cutoff := svc.clock.Now().Add(-svc.config.Osquery.LabelUpdateInterval)
|
2016-09-26 17:14:39 +00:00
|
|
|
labelQueries, err := svc.ds.LabelQueriesForHost(&host, cutoff)
|
2016-09-21 03:08:11 +00:00
|
|
|
if err != nil {
|
2017-03-21 16:17:38 +00:00
|
|
|
return nil, 0, osqueryError{message: "retrieving label queries: " + err.Error()}
|
2016-09-21 03:08:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for name, query := range labelQueries {
|
|
|
|
queries[hostLabelQueryPrefix+name] = query
|
|
|
|
}
|
2016-09-04 05:13:42 +00:00
|
|
|
|
2016-11-14 18:22:54 +00:00
|
|
|
distributedQueries, err := svc.ds.DistributedQueriesForHost(&host)
|
|
|
|
if err != nil {
|
2017-03-21 16:17:38 +00:00
|
|
|
return nil, 0, osqueryError{message: "retrieving query campaigns: " + err.Error()}
|
2016-11-14 18:22:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for id, query := range distributedQueries {
|
|
|
|
queries[hostDistributedQueryPrefix+strconv.Itoa(int(id))] = query
|
|
|
|
}
|
2016-09-04 05:13:42 +00:00
|
|
|
|
2017-03-21 16:17:38 +00:00
|
|
|
accelerate := uint(0)
|
|
|
|
if host.HostName == "" && host.Platform == "" {
|
|
|
|
// Assume this host is just enrolling, and accelerate checkins
|
|
|
|
// (to allow for platform restricted labels to run quickly
|
|
|
|
// after platform is retrieved from details)
|
|
|
|
accelerate = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
return queries, accelerate, nil
|
2016-09-04 05:13:42 +00:00
|
|
|
}
|
|
|
|
|
2016-10-05 00:17:55 +00:00
|
|
|
// ingestDetailQuery takes the results of a detail query and modifies the
|
|
|
|
// provided kolide.Host appropriately.
|
|
|
|
func (svc service) ingestDetailQuery(host *kolide.Host, name string, rows []map[string]string) error {
|
|
|
|
trimmedQuery := strings.TrimPrefix(name, hostDetailQueryPrefix)
|
|
|
|
query, ok := detailQueries[trimmedQuery]
|
|
|
|
if !ok {
|
|
|
|
return osqueryError{message: "unknown detail query " + trimmedQuery}
|
|
|
|
}
|
|
|
|
|
2017-01-19 23:28:33 +00:00
|
|
|
err := query.IngestFunc(svc.logger, host, rows)
|
2016-10-05 00:17:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return osqueryError{
|
|
|
|
message: fmt.Sprintf("ingesting query %s: %s", name, err.Error()),
|
|
|
|
}
|
|
|
|
}
|
2016-12-01 17:00:00 +00:00
|
|
|
|
2016-10-05 00:17:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-10-05 15:56:29 +00:00
|
|
|
// ingestLabelQuery records the results of label queries run by a host
|
2017-01-17 06:03:51 +00:00
|
|
|
func (svc service) ingestLabelQuery(host kolide.Host, query string, rows []map[string]string, results map[uint]bool) error {
|
2016-10-05 00:17:55 +00:00
|
|
|
trimmedQuery := strings.TrimPrefix(query, hostLabelQueryPrefix)
|
2017-06-08 17:57:12 +00:00
|
|
|
trimmedQueryNum, err := strconv.Atoi(emptyToZero(trimmedQuery))
|
2017-01-17 06:03:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "converting query from string to int")
|
|
|
|
}
|
2016-10-05 15:56:29 +00:00
|
|
|
// A label query matches if there is at least one result for that
|
|
|
|
// query. We must also store negative results.
|
2017-01-17 06:03:51 +00:00
|
|
|
results[uint(trimmedQueryNum)] = len(rows) > 0
|
2016-10-05 00:17:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-11-14 18:22:54 +00:00
|
|
|
// ingestDistributedQuery takes the results of a distributed query and modifies the
|
|
|
|
// provided kolide.Host appropriately.
|
Push distributed query errors over results websocket (#878)
As of recently, osquery will report when a distributed query fails. We now
expose errors over the results websocket. When a query errored on the host, the
`error` key in the result will be non-null. Note that osquery currently doesn't
provide any details so the error string will always be "failed". I anticipate
that we will fix this and the string is included for future-proofing.
Successful result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 15,
"host": {
... omitted ...
},
"rows": [
{
"hour": "1"
}
],
"error": null
}
}
```
Failed result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 14,
"host": {
... omitted ...
},
"rows": [
],
"error": "failed"
}
}
```
2017-01-11 03:34:32 +00:00
|
|
|
func (svc service) ingestDistributedQuery(host kolide.Host, name string, rows []map[string]string, failed bool) error {
|
2016-11-14 18:22:54 +00:00
|
|
|
trimmedQuery := strings.TrimPrefix(name, hostDistributedQueryPrefix)
|
|
|
|
|
2017-06-08 17:57:12 +00:00
|
|
|
campaignID, err := strconv.Atoi(emptyToZero(trimmedQuery))
|
2016-11-14 18:22:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "unable to parse campaign ID: " + trimmedQuery}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the results to the pubsub store
|
|
|
|
res := kolide.DistributedQueryResult{
|
|
|
|
DistributedQueryCampaignID: uint(campaignID),
|
2019-07-16 22:41:50 +00:00
|
|
|
Host: host,
|
|
|
|
Rows: rows,
|
2016-11-14 18:22:54 +00:00
|
|
|
}
|
Push distributed query errors over results websocket (#878)
As of recently, osquery will report when a distributed query fails. We now
expose errors over the results websocket. When a query errored on the host, the
`error` key in the result will be non-null. Note that osquery currently doesn't
provide any details so the error string will always be "failed". I anticipate
that we will fix this and the string is included for future-proofing.
Successful result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 15,
"host": {
... omitted ...
},
"rows": [
{
"hour": "1"
}
],
"error": null
}
}
```
Failed result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 14,
"host": {
... omitted ...
},
"rows": [
],
"error": "failed"
}
}
```
2017-01-11 03:34:32 +00:00
|
|
|
if failed {
|
|
|
|
// osquery errors are not currently helpful, but we should fix
|
|
|
|
// them to be better in the future
|
|
|
|
errString := "failed"
|
|
|
|
res.Error = &errString
|
|
|
|
}
|
2016-11-14 18:22:54 +00:00
|
|
|
|
|
|
|
err = svc.resultStore.WriteResult(res)
|
|
|
|
if err != nil {
|
2016-12-27 15:35:19 +00:00
|
|
|
nErr, ok := err.(pubsub.Error)
|
|
|
|
if !ok || !nErr.NoSubscriber() {
|
|
|
|
return osqueryError{message: "writing results: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are no subscribers, the campaign is "orphaned"
|
|
|
|
// and should be closed so that we don't continue trying to
|
|
|
|
// execute that query when we can't write to any subscriber
|
|
|
|
campaign, err := svc.ds.DistributedQueryCampaign(uint(campaignID))
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "loading orphaned campaign: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
campaign.Status = kolide.QueryComplete
|
|
|
|
if err := svc.ds.SaveDistributedQueryCampaign(campaign); err != nil {
|
|
|
|
return osqueryError{message: "closing orphaned campaign: " + err.Error()}
|
|
|
|
}
|
2016-11-14 18:22:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Record execution of the query
|
Push distributed query errors over results websocket (#878)
As of recently, osquery will report when a distributed query fails. We now
expose errors over the results websocket. When a query errored on the host, the
`error` key in the result will be non-null. Note that osquery currently doesn't
provide any details so the error string will always be "failed". I anticipate
that we will fix this and the string is included for future-proofing.
Successful result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 15,
"host": {
... omitted ...
},
"rows": [
{
"hour": "1"
}
],
"error": null
}
}
```
Failed result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 14,
"host": {
... omitted ...
},
"rows": [
],
"error": "failed"
}
}
```
2017-01-11 03:34:32 +00:00
|
|
|
status := kolide.ExecutionSucceeded
|
|
|
|
if failed {
|
|
|
|
status = kolide.ExecutionFailed
|
|
|
|
}
|
2016-11-16 13:47:49 +00:00
|
|
|
exec := &kolide.DistributedQueryExecution{
|
2019-07-16 22:41:50 +00:00
|
|
|
HostID: host.ID,
|
2016-11-14 18:22:54 +00:00
|
|
|
DistributedQueryCampaignID: uint(campaignID),
|
2019-07-16 22:41:50 +00:00
|
|
|
Status: status,
|
2016-11-14 18:22:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = svc.ds.NewDistributedQueryExecution(exec)
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "recording execution: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-24 19:40:11 +00:00
|
|
|
func (svc service) SubmitDistributedQueryResults(ctx context.Context, results kolide.OsqueryDistributedQueryResults, statuses map[string]kolide.OsqueryStatus) error {
|
2016-10-05 00:17:55 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
2016-12-06 19:51:11 +00:00
|
|
|
|
2016-10-05 00:17:55 +00:00
|
|
|
if !ok {
|
|
|
|
return osqueryError{message: "internal error: missing host from request context"}
|
|
|
|
}
|
|
|
|
|
2017-03-30 15:31:05 +00:00
|
|
|
var err error
|
2020-05-21 15:36:00 +00:00
|
|
|
detailUpdated := false // Whether detail or additional was updated
|
|
|
|
additionalResults := make(kolide.OsqueryDistributedQueryResults)
|
2017-01-17 06:03:51 +00:00
|
|
|
labelResults := map[uint]bool{}
|
2016-10-05 00:17:55 +00:00
|
|
|
for query, rows := range results {
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(query, hostDetailQueryPrefix):
|
|
|
|
err = svc.ingestDetailQuery(&host, query, rows)
|
2017-01-11 18:48:24 +00:00
|
|
|
detailUpdated = true
|
2020-05-21 15:36:00 +00:00
|
|
|
case strings.HasPrefix(query, hostAdditionalQueryPrefix):
|
|
|
|
name := strings.TrimPrefix(query, hostAdditionalQueryPrefix)
|
|
|
|
additionalResults[name] = rows
|
|
|
|
detailUpdated = true
|
2016-10-05 00:17:55 +00:00
|
|
|
case strings.HasPrefix(query, hostLabelQueryPrefix):
|
2016-10-05 15:56:29 +00:00
|
|
|
err = svc.ingestLabelQuery(host, query, rows, labelResults)
|
2016-11-14 18:22:54 +00:00
|
|
|
case strings.HasPrefix(query, hostDistributedQueryPrefix):
|
Push distributed query errors over results websocket (#878)
As of recently, osquery will report when a distributed query fails. We now
expose errors over the results websocket. When a query errored on the host, the
`error` key in the result will be non-null. Note that osquery currently doesn't
provide any details so the error string will always be "failed". I anticipate
that we will fix this and the string is included for future-proofing.
Successful result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 15,
"host": {
... omitted ...
},
"rows": [
{
"hour": "1"
}
],
"error": null
}
}
```
Failed result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 14,
"host": {
... omitted ...
},
"rows": [
],
"error": "failed"
}
}
```
2017-01-11 03:34:32 +00:00
|
|
|
// osquery docs say any nonzero (string) value for
|
|
|
|
// status indicates a query error
|
|
|
|
status, ok := statuses[query]
|
2018-04-24 19:40:11 +00:00
|
|
|
failed := (ok && status != kolide.StatusOK)
|
Push distributed query errors over results websocket (#878)
As of recently, osquery will report when a distributed query fails. We now
expose errors over the results websocket. When a query errored on the host, the
`error` key in the result will be non-null. Note that osquery currently doesn't
provide any details so the error string will always be "failed". I anticipate
that we will fix this and the string is included for future-proofing.
Successful result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 15,
"host": {
... omitted ...
},
"rows": [
{
"hour": "1"
}
],
"error": null
}
}
```
Failed result:
```
{
"type": "result",
"data": {
"distributed_query_execution_id": 14,
"host": {
... omitted ...
},
"rows": [
],
"error": "failed"
}
}
```
2017-01-11 03:34:32 +00:00
|
|
|
err = svc.ingestDistributedQuery(host, query, rows, failed)
|
2016-10-05 00:17:55 +00:00
|
|
|
default:
|
2016-11-14 18:22:54 +00:00
|
|
|
err = osqueryError{message: "unknown query prefix: " + query}
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "failed to ingest result: " + err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2016-10-05 15:56:29 +00:00
|
|
|
if len(labelResults) > 0 {
|
|
|
|
err = svc.ds.RecordLabelQueryExecutions(&host, labelResults, svc.clock.Now())
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "failed to save labels: " + err.Error()}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-11 18:48:24 +00:00
|
|
|
if detailUpdated {
|
|
|
|
host.DetailUpdateTime = svc.clock.Now()
|
2020-05-21 15:36:00 +00:00
|
|
|
additionalJSON, err := json.Marshal(additionalResults)
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "failed to marshal additional: " + err.Error()}
|
|
|
|
}
|
|
|
|
additional := json.RawMessage(additionalJSON)
|
|
|
|
host.Additional = &additional
|
2017-01-11 18:48:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(labelResults) > 0 || detailUpdated {
|
|
|
|
err = svc.ds.SaveHost(&host)
|
|
|
|
if err != nil {
|
|
|
|
return osqueryError{message: "failed to update host details: " + err.Error()}
|
|
|
|
}
|
2016-10-05 00:17:55 +00:00
|
|
|
}
|
|
|
|
|
2016-09-04 05:13:42 +00:00
|
|
|
return nil
|
|
|
|
}
|