diff --git a/changes/issue-6475-host-operating-system b/changes/issue-6475-host-operating-system new file mode 100644 index 0000000000..b8a6df727a --- /dev/null +++ b/changes/issue-6475-host-operating-system @@ -0,0 +1,2 @@ +- Added new host detail queries to ingest operating system name, version, architecture, and + kernel version diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 841878a8ca..e150d67a6d 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -682,6 +682,22 @@ func (a *agent) batteries() []map[string]string { return result } +func (a *agent) osUnixLike() []map[string]string { + return []map[string]string{ + { + "build": "18G3020", + "major": "10", + "minor": "14", + "name": "Mac OS X", + "patch": "6", + "platform": "darwin", + "arch": "x86_64", + "version": "10.14.6", + "kernel_version": "21.4.0", + }, + } +} + func (a *agent) processQuery(name, query string) (handled bool, results []map[string]string, status *fleet.OsqueryStatus) { const ( hostPolicyQueryPrefix = "fleet_policy_query_" @@ -723,6 +739,13 @@ func (a *agent) processQuery(name, query string) (handled bool, results []map[st results = a.batteries() } return true, results, &ss + case name == hostDetailQueryPrefix+"os_unix_like": + ss := fleet.OsqueryStatus(rand.Intn(2)) + if ss == fleet.StatusOK { + results = a.osUnixLike() + } + return true, results, &ss + default: // Look for results in the template file. if t := a.templates.Lookup(name); t == nil { diff --git a/go.mod b/go.mod index c0ee46dfc9..79bb4ed08f 100644 --- a/go.mod +++ b/go.mod @@ -222,6 +222,7 @@ require ( github.com/jonboulle/clockwork v0.2.2 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/klauspost/compress v1.15.0 // indirect + github.com/lib/pq v1.10.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.11 // indirect @@ -274,6 +275,7 @@ require ( github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/zclconf/go-cty v1.1.0 // indirect + github.com/ziutek/mymysql v1.5.4 // indirect go.elastic.co/apm v1.15.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect go.opencensus.io v0.23.0 // indirect diff --git a/go.sum b/go.sum index 4502d7c134..7a2a75edb1 100644 --- a/go.sum +++ b/go.sum @@ -1731,6 +1731,7 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= diff --git a/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable.go b/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable.go new file mode 100644 index 0000000000..4b5992b384 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable.go @@ -0,0 +1,46 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20220713091130, Down_20220713091130) +} + +func Up_20220713091130(tx *sql.Tx) error { + // Length of VARCHAR set to conform with max key length limitations for the unique constraint + _, err := tx.Exec(` +CREATE TABLE operating_systems ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + version VARCHAR(150) NOT NULL, + arch VARCHAR(150) NOT NULL, + kernel_version VARCHAR(150) NOT NULL, + platform VARCHAR(50) NOT NULL, + UNIQUE KEY idx_unique_os (name, version, arch, kernel_version, platform) +) + `) + if err != nil { + return errors.Wrapf(err, "create operating_systems table") + } + + _, err = tx.Exec(` +CREATE TABLE host_operating_system ( + host_id INT UNSIGNED NOT NULL PRIMARY KEY, + os_id INT UNSIGNED NOT NULL, + FOREIGN KEY fk_operating_systems_id (os_id) REFERENCES operating_systems(id) ON DELETE CASCADE, + INDEX idx_host_operating_system_id (os_id) +)`) + if err != nil { + return errors.Wrapf(err, "create host_operating_systems table") + } + + return nil +} + +func Down_20220713091130(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable_test.go b/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable_test.go new file mode 100644 index 0000000000..975399278d --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20220713091130_AddOperatingSystemsTable_test.go @@ -0,0 +1,104 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20220713091130(t *testing.T) { + db := applyUpToPrev(t) + + applyNext(t, db) + + // test operating systems table + stmt := ` +INSERT INTO operating_systems ( + name, + version, + arch, + kernel_version, + platform +) +VALUES (?, ?, ?, ?, ?) +` + + var ( + name string + version string + arch string + kernel_version string + platform string + ) + + // add first operating system + _, err := db.Exec(stmt, "Ubuntu", "22.04 LTS", "x86_64", "5.10.76-linuxkit", "ubuntu") + require.NoError(t, err) + + err = db.QueryRow(`SELECT name, version, arch, kernel_version, platform FROM operating_systems WHERE name = ? AND version = ?`, "Ubuntu", "22.04 LTS"). + Scan(&name, &version, &arch, &kernel_version, &platform) + require.NoError(t, err) + require.Equal(t, "Ubuntu", name) + require.Equal(t, "22.04 LTS", version) + require.Equal(t, "x86_64", arch) + require.Equal(t, "5.10.76-linuxkit", kernel_version) + require.Equal(t, "ubuntu", platform) + + // add second operating system + _, err = db.Exec(stmt, "Ubuntu", "22.06 LTS", "x86_64", "5.10.76-linuxkit", "ubuntu") + require.NoError(t, err) + + err = db.QueryRow(`SELECT name, version, arch, kernel_version, platform FROM operating_systems WHERE name = ? AND version = ?`, "Ubuntu", "22.06 LTS"). + Scan(&name, &version, &arch, &kernel_version, &platform) + require.NoError(t, err) + require.Equal(t, "Ubuntu", name) + require.Equal(t, "22.06 LTS", version) + require.Equal(t, "x86_64", arch) + require.Equal(t, "5.10.76-linuxkit", kernel_version) + require.Equal(t, "ubuntu", platform) + + // test host operating systems table + stmt = ` +INSERT INTO host_operating_system ( + host_id, + os_id +) +VALUES (?, ?) +` + // new host id, new os id + _, err = db.Exec(stmt, 111, 1) + require.NoError(t, err) + + // new host id, new os id + _, err = db.Exec(stmt, 222, 2) + require.NoError(t, err) + + // new host id, duplicate os id + _, err = db.Exec(stmt, 333, 2) + require.NoError(t, err) + + // new host id, non-existent os id, foreign key error + _, err = db.Exec(stmt, 444, 4) + require.Error(t, err) + + // duplicate host id, new os id, primary key error + _, err = db.Exec(stmt, 111, 4) + require.Error(t, err) + + // duplicate host id, duplicate os id, primary key error + _, err = db.Exec(stmt, 111, 2) + require.Error(t, err) + + var osID uint + err = db.QueryRow(`SELECT os_id FROM host_operating_system WHERE host_id = 111`). + Scan(&osID) + require.NoError(t, err) + require.Equal(t, uint(1), osID) + + var hostIDs []int + err = db.Select(&hostIDs, `SELECT host_id FROM host_operating_system WHERE os_id = 2`) + require.NoError(t, err) + require.Len(t, hostIDs, 2) + require.Contains(t, hostIDs, 222) + require.Contains(t, hostIDs, 333) +} diff --git a/server/datastore/mysql/operating_systems.go b/server/datastore/mysql/operating_systems.go new file mode 100644 index 0000000000..d9927bb7a8 --- /dev/null +++ b/server/datastore/mysql/operating_systems.go @@ -0,0 +1,126 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/jmoiron/sqlx" +) + +func (ds *Datastore) ListOperatingSystems(ctx context.Context) ([]fleet.OperatingSystem, error) { + return listOperatingSystemsDB(ctx, ds.reader) +} + +func listOperatingSystemsDB(ctx context.Context, tx sqlx.QueryerContext) ([]fleet.OperatingSystem, error) { + var os []fleet.OperatingSystem + if err := sqlx.SelectContext(ctx, tx, &os, `SELECT id, name, version, arch, kernel_version FROM operating_systems`); err != nil { + return nil, err + } + return os, nil +} + +func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error { + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + os, err := getOrGenerateOperatingSystemDB(ctx, ds.writer, hostOS) + if err != nil { + return err + } + return upsertHostOperatingSystemDB(ctx, ds.writer, hostID, os.ID) + }) +} + +// getOrGenerateOperatingSystemDB queries the `operating_systems` table with the +// name, version, arch, and kernel_version of the given operating system. If found, +// it returns the record including the associated ID. If not found, it returns a call +// to `newOperatingSystemDB`, which inserts a new record and returns the record +// including the newly associated ID. +func getOrGenerateOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.OperatingSystem) (*fleet.OperatingSystem, error) { + switch os, err := getOperatingSystemDB(ctx, tx, hostOS); { + case err == nil: + return os, nil + case errors.Is(err, sql.ErrNoRows): + return newOperatingSystemDB(ctx, tx, hostOS) + default: + return nil, ctxerr.Wrap(ctx, err, "get operating system") + } +} + +// `newOperatingSystemDB` inserts a record for the given operating system and +// returns the record including the newly associated ID. +func newOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.OperatingSystem) (*fleet.OperatingSystem, error) { + stmt := "INSERT IGNORE INTO operating_systems (name, version, arch, kernel_version, platform) VALUES (?, ?, ?, ?, ?)" + if _, err := tx.ExecContext(ctx, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion, hostOS.Platform); err != nil { + return nil, ctxerr.Wrap(ctx, err, "insert new operating system") + } + + // With INSERT IGNORE, attempting a duplicate insert will return LastInsertId of 0 + // so we retrieve the stored record to guard against that case. + switch storedOS, err := getOperatingSystemDB(ctx, tx, hostOS); { + case err == nil: + return storedOS, nil + case errors.Is(err, sql.ErrNoRows): + return nil, doRetryErr + default: + return nil, ctxerr.Wrap(ctx, err, "get new operating system") + } +} + +// getOperatingSystemDB queries the `operating_systems` table with the +// name, version, arch, and kernel_version of the given operating system. +// If found, it returns the record including the associated ID. +func getOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.OperatingSystem) (*fleet.OperatingSystem, error) { + var os fleet.OperatingSystem + stmt := "SELECT id, name, version, arch, kernel_version FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ?" + if err := sqlx.GetContext(ctx, tx, &os, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion); err != nil { + return nil, err + } + return &os, nil +} + +// upsertHostOperatingSystemDB upserts the host operating system table +// with the operating system id for the given host ID +func upsertHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint, osID uint) error { + res, err := tx.ExecContext(ctx, "UPDATE host_operating_system SET os_id = ? WHERE host_id = ?", osID, hostID) + if err != nil { + return err + } + + if n, _ := res.RowsAffected(); n > 0 { + // update success + return nil + } + + // no row to update so insert new row + _, err = tx.ExecContext(ctx, "INSERT INTO host_operating_system (host_id, os_id) VALUES (?, ?)", hostID, osID) + if err != nil { + return err + } + + return nil +} + +// getIDHostOperatingSystemDB queries the `host_operating_system` table and returns the +// operating system ID for the given host ID. +func getIDHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (uint, error) { + var id uint + stmt := "SELECT os_id FROM host_operating_system WHERE host_id = ?" + if err := sqlx.GetContext(ctx, tx, &id, stmt, hostID); err != nil { + return 0, err + } + return id, nil +} + +// getIDHostOperatingSystemDB queries the `operating_systems` table and returns the +// operating system record associated with the given host ID based on a subquery +// of the `host_operating_system` table. +func getHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (*fleet.OperatingSystem, error) { + var os fleet.OperatingSystem + stmt := "SELECT id, name, version, arch, kernel_version FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)" + if err := sqlx.GetContext(ctx, tx, &os, stmt, hostID); err != nil { + return nil, err + } + return &os, nil +} diff --git a/server/datastore/mysql/operating_systems_test.go b/server/datastore/mysql/operating_systems_test.go new file mode 100644 index 0000000000..dcfea4767f --- /dev/null +++ b/server/datastore/mysql/operating_systems_test.go @@ -0,0 +1,298 @@ +package mysql + +import ( + "context" + "database/sql" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListOperatingSystems(t *testing.T) { + ctx := context.Background() + ds := CreateMySQLDS(t) + + // no os records + list, err := ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 0) + + // with os records + seedByID := seedOperatingSystems(t, ds) + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, len(seedByID)) + + osByID := make(map[uint]fleet.OperatingSystem) + for _, os := range list { + osByID[os.ID] = os + } + for _, os := range osByID { + require.Equal(t, os, seedByID[os.ID]) + } +} + +func TestUpdateHostOperatingSystem(t *testing.T) { + ctx := context.Background() + ds := CreateMySQLDS(t) + + testHostID := uint(42) + testOS := fleet.OperatingSystem{ + Name: "Ubuntu", + Version: "16.04.7 LTS", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + Platform: "ubuntu", + } + + // no records + list, err := ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 0) + _, err = getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.ErrorIs(t, err, sql.ErrNoRows) + + // insert new os record and host operating system record + err = ds.UpdateHostOperatingSystem(ctx, testHostID, testOS) + require.NoError(t, err) + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 1) + require.Equal(t, true, isSameOS(t, testOS, list[0])) + storedOS, err := getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, true, isSameOS(t, testOS, *storedOS)) + + // new version creates a new os record + testNewVersion := testOS + testNewVersion.Version = "22.04 LTS" + err = ds.UpdateHostOperatingSystem(ctx, testHostID, testNewVersion) + require.NoError(t, err) + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 2) + storedOS, err = getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, true, isSameOS(t, testNewVersion, *storedOS)) + + // new host with existing os + testNewHostID := uint(43) + err = ds.UpdateHostOperatingSystem(ctx, testNewHostID, testOS) + require.NoError(t, err) + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 2) + storedOS, err = getHostOperatingSystemDB(ctx, ds.writer, testNewHostID) + require.NoError(t, err) + require.Equal(t, true, isSameOS(t, testOS, *storedOS)) + + // no change + err = ds.UpdateHostOperatingSystem(ctx, testNewHostID, testOS) + require.NoError(t, err) + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Len(t, list, 2) + storedOS, err = getHostOperatingSystemDB(ctx, ds.writer, testNewHostID) + require.NoError(t, err) + require.Equal(t, true, isSameOS(t, testOS, *storedOS)) +} + +func TestMaybeNewOperatingSystem(t *testing.T) { + ctx := context.Background() + ds := CreateMySQLDS(t) + + seedOperatingSystems(t, ds) + list, err := ds.ListOperatingSystems(ctx) + require.NoError(t, err) + osByID := make(map[uint]fleet.OperatingSystem) + for _, os := range list { + osByID[os.ID] = os + } + + testOS := fleet.OperatingSystem{ + Name: "Ubuntu", + Version: "16.04.7 LTS", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + Platform: "ubuntu", + } + + // new os, returns a newly inserted record + result1, err := getOrGenerateOperatingSystemDB(ctx, ds.writer, testOS) + require.NoError(t, err) + require.True(t, isSameOS(t, testOS, *result1)) + require.NotContains(t, osByID, result1.ID) + + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Equal(t, len(osByID)+1, len(list)) + + osByID = make(map[uint]fleet.OperatingSystem) + for _, os := range list { + osByID[os.ID] = os + } + require.Contains(t, osByID, result1.ID) + require.True(t, isSameOS(t, osByID[result1.ID], testOS)) + + // no change, returns the existing record + result2, err := getOrGenerateOperatingSystemDB(ctx, ds.writer, testOS) + require.NoError(t, err) + require.True(t, isSameOS(t, *result1, *result2)) + + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Equal(t, len(osByID), len(list)) + + osByID = make(map[uint]fleet.OperatingSystem) + for _, os := range list { + osByID[os.ID] = os + } + require.Contains(t, osByID, result2.ID) + require.True(t, isSameOS(t, osByID[result2.ID], testOS)) + + // new version, returns new record + testNewVersion := testOS + testNewVersion.Version = "22.04 LTS" + result3, err := getOrGenerateOperatingSystemDB(ctx, ds.writer, testNewVersion) + require.NoError(t, err) + require.True(t, isSameOS(t, testNewVersion, *result3)) + + list, err = ds.ListOperatingSystems(ctx) + require.NoError(t, err) + require.Equal(t, len(osByID)+1, len(list)) + + osByID = make(map[uint]fleet.OperatingSystem) + for _, os := range list { + osByID[os.ID] = os + } + require.Contains(t, osByID, result3.ID) + require.True(t, isSameOS(t, osByID[result3.ID], testNewVersion)) + require.Contains(t, osByID, result2.ID) + require.True(t, isSameOS(t, osByID[result2.ID], testOS)) +} + +func TestMaybeUpdateHostOperatingSystem(t *testing.T) { + ctx := context.Background() + ds := CreateMySQLDS(t) + + seedOperatingSystems(t, ds) + osList, err := ds.ListOperatingSystems(ctx) + require.NoError(t, err) + + testHostID := uint(42) + + // no record exists for test host + _, err = getIDHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.ErrorIs(t, err, sql.ErrNoRows) + + // insert test host and os id + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[0].ID) + require.NoError(t, err) + osID, err := getIDHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[0].ID, osID) + + // update test host with new os id + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[1].ID) + require.NoError(t, err) + osID, err = getIDHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1].ID, osID) + + // no change + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[1].ID) + require.NoError(t, err) + osID, err = getIDHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1].ID, osID) +} + +func TestGetHostOperatingSystem(t *testing.T) { + ctx := context.Background() + ds := CreateMySQLDS(t) + + seedOperatingSystems(t, ds) + osList, err := ds.ListOperatingSystems(ctx) + require.NoError(t, err) + + testHostID := uint(42) + + // no record exists for test host + _, err = getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.ErrorIs(t, err, sql.ErrNoRows) + + // insert test host and os id + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[0].ID) + require.NoError(t, err) + os, err := getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[0], *os) + + // update test host with new os id + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[1].ID) + require.NoError(t, err) + os, err = getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) + + // no change + err = upsertHostOperatingSystemDB(ctx, ds.writer, testHostID, osList[1].ID) + require.NoError(t, err) + os, err = getHostOperatingSystemDB(ctx, ds.writer, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) +} + +func seedOperatingSystems(t *testing.T, ds *Datastore) map[uint]fleet.OperatingSystem { + osSeeds := []fleet.OperatingSystem{ + { + Name: "Microsoft Windows 11 Enterprise Evaluation", + Version: "21H2", + Arch: "64-bit", + KernelVersion: "10.0.22000.795", + Platform: "windows", + }, + { + Name: "macOS", + Version: "12.3.1", + Arch: "x86_64", + KernelVersion: "21.4.0", + Platform: "darwin", + }, + { + Name: "Ubuntu", + Version: "20.04.2 LTS", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + Platform: "ubuntu", + }, + { + Name: "Debian GNU/Linux", + Version: "10.0.0", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + Platform: "debian", + }, + { + Name: "CentOS Linux", + Version: "8.3.2011", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + Platform: "rhel", + }, + } + storedById := make(map[uint]fleet.OperatingSystem) + for _, os := range osSeeds { + stored, err := newOperatingSystemDB(context.Background(), ds.writer, os) + require.NoError(t, err) + require.True(t, isSameOS(t, os, *stored)) + storedById[stored.ID] = *stored + } + return storedById +} + +func isSameOS(t *testing.T, os1 fleet.OperatingSystem, os2 fleet.OperatingSystem) bool { + return assert.ElementsMatch(t, []string{os1.Name, os1.Version, os1.Arch, os1.KernelVersion}, []string{os2.Name, os2.Version, os2.Arch, os2.KernelVersion}) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index f45236a37e..1e3bcd7159 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -192,6 +192,16 @@ CREATE TABLE `host_munki_info` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `host_operating_system` ( + `host_id` int(10) unsigned NOT NULL, + `os_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`host_id`), + KEY `idx_host_operating_system_id` (`os_id`), + CONSTRAINT `host_operating_system_ibfk_1` FOREIGN KEY (`os_id`) REFERENCES `operating_systems` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `host_seen_times` ( `host_id` int(10) unsigned NOT NULL, `seen_time` timestamp NULL DEFAULT NULL, @@ -358,7 +368,7 @@ CREATE TABLE `locks` ( `expires_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -369,9 +379,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=142 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=143 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `network_interfaces` ( @@ -404,6 +414,19 @@ CREATE TABLE `network_interfaces` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `operating_systems` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `version` varchar(150) NOT NULL, + `arch` varchar(150) NOT NULL, + `kernel_version` varchar(150) NOT NULL, + `platform` varchar(50) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_unique_os` (`name`,`version`,`arch`,`kernel_version`,`platform`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `osquery_options` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `override_type` int(1) NOT NULL, diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index e674cff7ce..23ad412ee8 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -391,6 +391,21 @@ type Datastore interface { InsertCVEMeta(ctx context.Context, cveMeta []CVEMeta) error ListCVEs(ctx context.Context, maxAge time.Duration) ([]CVEMeta, error) + /////////////////////////////////////////////////////////////////////////////// + // OperatingSystemsStore + + // ListOperationsSystems returns all operating systems (id, name, version) + ListOperatingSystems(ctx context.Context) ([]OperatingSystem, error) + // UpdateHostOperatingSystem updates the `host_operating_system` table + // for the given host ID with the ID of the operating system associated + // with the given name, version, arch, and kernel version in the + // `operating_systems` table. + // + // If the `operating_systems` table does not already include a record + // associated with the given name, version, arch, and kernel version, + // a new record is also created. + UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS OperatingSystem) error + /////////////////////////////////////////////////////////////////////////////// // ActivitiesStore diff --git a/server/fleet/operating_systems.go b/server/fleet/operating_systems.go new file mode 100644 index 0000000000..2b595ab8a1 --- /dev/null +++ b/server/fleet/operating_systems.go @@ -0,0 +1,16 @@ +package fleet + +// OperatingSystem is an operating system uniquely identified according to its name and version. +type OperatingSystem struct { + ID uint `json:"id" db:"id"` + // Name is the name of the operating system, e.g., "Debian/GNU Linus", "Ubuntu", or "Microsoft Windows 11 Enterprise" + Name string `json:"name" db:"name"` + // Version is the version of the operating system, e.g., "10.0.0", "22.04 LTS", "21H2" + Version string `json:"version" db:"version"` + // Arch is the architecture of the operating system, e.g., "x86_64" or "64-bit" + Arch string `json:"arch,omitempty" db:"arch"` + // KernelVersion is the kernel version of the operating system, e.g., "5.10.76-linuxkit" or "10.0.22000.795" + KernelVersion string `json:"kernel_version,omitempty" db:"kernel_version"` + // Platform is the platform of the operating system, e.g., "darwin" or "rhel" + Platform string `json:"platform" db:"platform"` +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 6b5d29141a..43162b3094 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -311,6 +311,10 @@ type InsertCVEMetaFunc func(ctx context.Context, cveMeta []fleet.CVEMeta) error type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) +type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error) + +type UpdateHostOperatingSystemFunc func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error + type NewActivityFunc func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error type ListActivitiesFunc func(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Activity, error) @@ -871,6 +875,12 @@ type DataStore struct { ListCVEsFunc ListCVEsFunc ListCVEsFuncInvoked bool + ListOperatingSystemsFunc ListOperatingSystemsFunc + ListOperatingSystemsFuncInvoked bool + + UpdateHostOperatingSystemFunc UpdateHostOperatingSystemFunc + UpdateHostOperatingSystemFuncInvoked bool + NewActivityFunc NewActivityFunc NewActivityFuncInvoked bool @@ -1785,6 +1795,16 @@ func (s *DataStore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]fleet return s.ListCVEsFunc(ctx, maxAge) } +func (s *DataStore) ListOperatingSystems(ctx context.Context) ([]fleet.OperatingSystem, error) { + s.ListOperatingSystemsFuncInvoked = true + return s.ListOperatingSystemsFunc(ctx) +} + +func (s *DataStore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error { + s.UpdateHostOperatingSystemFuncInvoked = true + return s.UpdateHostOperatingSystemFunc(ctx, hostID, hostOS) +} + func (s *DataStore) NewActivity(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { s.NewActivityFuncInvoked = true return s.NewActivityFunc(ctx, user, activityType, details) diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 78938515bd..bbc860be05 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -187,7 +187,8 @@ func TestAgentOptionsForHost(t *testing.T) { assert.JSONEq(t, `{"foo":"override2"}`, string(opt)) } -// One of these queries is the disk space, only one of the two works in a platform +// One of these queries is the disk space, only one of the two works in a platform. Similarly, one +// is for operating system. var expectedDetailQueries = osquery_utils.GetDetailQueries(&fleet.AppConfig{HostSettings: fleet.HostSettings{EnableHostUsers: true}}, config.FleetConfig{}) func TestEnrollAgent(t *testing.T) { @@ -557,8 +558,9 @@ func TestHostDetailQueries(t *testing.T) { queries, discovery, err = svc.detailQueriesForHost(context.Background(), &host) require.NoError(t, err) - // +1 because 2 additional queries, but -1 due to removed disk space query (only 1 of 2 active for a given platform) - require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) + // 2 additional queries, but -2 expected queries due to removed disk space query (only 1 of 2 + // active for a given platform) and removed Windows-specific operating system query + require.Equal(t, len(expectedDetailQueries), len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) for name := range queries { assert.True(t, @@ -612,8 +614,9 @@ func TestLabelQueries(t *testing.T) { // should be turned on so that we can quickly fill labels) queries, discovery, acc, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // -1 due to removed disk space query (only 1 of 2 active for a given platform) - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // -2 expected queries due to removed disk space query (only 1 of 2 active for a given platform) + // and removed Windows-specific operating system query + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) assert.NotZero(t, acc) @@ -703,8 +706,9 @@ func TestLabelQueries(t *testing.T) { ctx = hostctx.NewContext(ctx, host) queries, discovery, acc, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // +3 for label queries, -1 due to removed disk space query (only 1 of 2 active for a given platform) - require.Equal(t, len(expectedDetailQueries)+2, len(queries), distQueriesMapKeys(queries)) + // +3 for label queries, but -2 expected queries due to removed disk space query (only 1 of 2 + // active for a given platform) and removed Windows-specific operating system query + require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) assert.Zero(t, acc) @@ -772,8 +776,9 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) { // queries) queries, discovery, acc, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // -4 due to windows not having battery, mdm, munki_info and removed disk space query (only 1 of 2 active for a given platform) - if !assert.Equal(t, len(expectedDetailQueries)-4, len(queries)) { + // -5 due to windows not having battery, mdm, munki_info and removed disk space query and + // operating system query (only 1 of 2 active for a given platform) + if !assert.Equal(t, len(expectedDetailQueries)-5, len(queries)) { // this is just to print the diff between the expected and actual query // keys when the count assertion fails, to help debugging - they are not // expected to match. @@ -932,8 +937,8 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) { require.NoError(t, err) // somehow confusingly, the query response above changed the host's platform // from windows to darwin, so now it has all expected queries except the - // extra disk space one. - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // extra disk space one and the windows-specific operating system query + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) assert.Zero(t, acc) } @@ -987,9 +992,9 @@ func TestDetailQueries(t *testing.T) { // queries) queries, discovery, acc, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // -4 due to linux platform, so battery, mdm and munki are missing, and the extra disk space query, - // then +1 due to software inventory being enabled. - if !assert.Equal(t, len(expectedDetailQueries)-3, len(queries)) { + // -5 due to linux platform, so battery, mdm, and munki are missing, and the extra disk space + // query, and the windows-specific operating system query, then +1 due to software inventory being enabled. + if !assert.Equal(t, len(expectedDetailQueries)-4, len(queries)) { // this is just to print the diff between the expected and actual query // keys when the count assertion fails, to help debugging - they are not // expected to match. @@ -1250,9 +1255,10 @@ func TestDetailQueries(t *testing.T) { queries, discovery, acc, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // host platform changed to darwin, so all queries are present - that is, -1 for the - // extra disk space query, +1 for the software inventory enabled. - require.Equal(t, len(expectedDetailQueries), len(queries), distQueriesMapKeys(queries)) + // host platform changed to darwin, so -2 for the + // extra disk space query and the windows-specific operating system query, + // +1 for the software inventory enabled. + require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) assert.Zero(t, acc) } @@ -1376,8 +1382,8 @@ func TestDistributedQueryResults(t *testing.T) { // Now we should get the active distributed query queries, discovery, acc, err := svc.GetDistributedQueries(hostCtx) require.NoError(t, err) - // -4 for the non-windows queries, +1 for the distributed query for campaign ID 42 - if !assert.Equal(t, len(expectedDetailQueries)-3, len(queries)) { + // -5 for the non-windows queries, +1 for the distributed query for campaign ID 42 + if !assert.Equal(t, len(expectedDetailQueries)-4, len(queries)) { // this is just to print the diff between the expected and actual query // keys when the count assertion fails, to help debugging - they are not // expected to match. @@ -2216,8 +2222,9 @@ func TestPolicyQueries(t *testing.T) { queries, discovery, _, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all queries -1 for the extra disk space one, and +2 for the policy queries - require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) + // all queries -2 for the extra disk space one and windows-specific operating system query, + // and +2 for the policy queries + require.Equal(t, len(expectedDetailQueries), len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) checkPolicyResults := func(queries map[string]string) { @@ -2273,8 +2280,8 @@ func TestPolicyQueries(t *testing.T) { ctx = hostctx.NewContext(context.Background(), host) queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // all standard queries minus the extra disk space and windows-specific operating system queries + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) noPolicyResults(queries) @@ -2283,8 +2290,9 @@ func TestPolicyQueries(t *testing.T) { queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space, +2 policy queries - require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) + // all standard queries -2 (the extra disk space and windows-specific operating system + // queries) and +2 policy queries + require.Equal(t, len(expectedDetailQueries), len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) checkPolicyResults(queries) @@ -2312,8 +2320,8 @@ func TestPolicyQueries(t *testing.T) { ctx = hostctx.NewContext(context.Background(), host) queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // all standard queries minus the extra disk space and windows-specific operating system queries + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) noPolicyResults(queries) @@ -2322,8 +2330,9 @@ func TestPolicyQueries(t *testing.T) { ctx = hostctx.NewContext(context.Background(), host) queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space, +2 policy queries - require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) + // all standard queries -2 (the extra disk space and windows-specific operating system + // queries) and +2 policy queries + require.Equal(t, len(expectedDetailQueries), len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) checkPolicyResults(queries) @@ -2353,8 +2362,8 @@ func TestPolicyQueries(t *testing.T) { ctx = hostctx.NewContext(context.Background(), host) queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // all standard queries minus the extra disk space and windows-specific operating system queries + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) noPolicyResults(queries) } @@ -2419,8 +2428,8 @@ func TestPolicyWebhooks(t *testing.T) { queries, discovery, _, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all queries -1 for extra disk space, +3 for policies - require.Equal(t, len(expectedDetailQueries)+2, len(queries), distQueriesMapKeys(queries)) + // all queries -2 for extra disk space and windows-specific operating system queries, +3 for policies + require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) checkPolicyResults := func(queries map[string]string) { @@ -2533,8 +2542,8 @@ func TestPolicyWebhooks(t *testing.T) { ctx = hostctx.NewContext(context.Background(), host) queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all standard queries minus the extra disk space - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // all standard queries minus the extra disk space and windows-specific operating system queries + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) noPolicyResults(queries) @@ -2543,8 +2552,8 @@ func TestPolicyWebhooks(t *testing.T) { queries, discovery, _, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all queries -1 for extra disk space, +3 for policies - require.Equal(t, len(expectedDetailQueries)+2, len(queries), distQueriesMapKeys(queries)) + // all queries -2 for extra disk space and windows-specific operating system queries, +3 for policies + require.Equal(t, len(expectedDetailQueries)+1, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) checkPolicyResults(queries) @@ -2667,8 +2676,8 @@ func TestLiveQueriesFailing(t *testing.T) { queries, discovery, _, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // all queries minus the extra disk space - require.Equal(t, len(expectedDetailQueries)-1, len(queries), distQueriesMapKeys(queries)) + // all standard queries minus the extra disk space and windows-specific operating system queries + require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) logs, err := ioutil.ReadAll(buf) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index ae74b310e9..c1202ad87a 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -130,32 +130,14 @@ var hostDetailQueries = map[string]DetailQuery{ return nil } - if strings.Contains(strings.ToLower(rows[0]["name"]), "ubuntu") { - // Ubuntu takes a different approach to updating patch IDs so we instead use - // the version string provided after removing the code name - regx := regexp.MustCompile(`\(.*\)`) - vers := regx.ReplaceAllString(rows[0]["version"], "") - host.OSVersion = fmt.Sprintf( - "%s %s", - rows[0]["name"], - strings.TrimSpace(vers), - ) - } else if rows[0]["major"] != "0" || rows[0]["minor"] != "0" || rows[0]["patch"] != "0" { - host.OSVersion = fmt.Sprintf( - "%s %s.%s.%s", - rows[0]["name"], - rows[0]["major"], - rows[0]["minor"], - rows[0]["patch"], - ) - } else { - host.OSVersion = fmt.Sprintf( - "%s %s", - rows[0]["name"], - rows[0]["build"], - ) - } - host.OSVersion = strings.Trim(host.OSVersion, ".") + host.OSVersion = fmt.Sprintf("%v %v", rows[0]["name"], parseOSVersion( + rows[0]["name"], + rows[0]["version"], + rows[0]["major"], + rows[0]["minor"], + rows[0]["patch"], + rows[0]["build"], + )) if build, ok := rows[0]["build"]; ok { host.Build = build @@ -352,6 +334,46 @@ var extraDetailQueries = map[string]DetailQuery{ // osquery table on darwin (https://osquery.io/schema/5.3.0#battery), it is // always present. }, + "os_windows": { + Query: ` + SELECT + os.name, + os.arch, + os.platform, + r.version AS version, + k.version AS kernel_version + FROM + os_version os, + kernel_info k, + ( + SELECT + data AS version + FROM + registry + WHERE + path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion') r`, + Platforms: []string{"windows"}, + DirectIngestFunc: directIngestOSWindows, + }, + "os_unix_like": { + Query: ` + SELECT + os.name, + os.major, + os.minor, + os.patch, + os.build, + os.arch, + os.platform, + os.version AS version, + k.version AS kernel_version + FROM + os_version os, + kernel_info k`, + Platforms: append(fleet.HostLinuxOSs, "darwin"), + DirectIngestFunc: directIngestOSUnixLike, + }, + OrbitInfoQueryName: OrbitInfoDetailQuery, } @@ -618,6 +640,78 @@ var usersQuery = DetailQuery{ DirectIngestFunc: directIngestUsers, } +// directIngestOSWindows ingests selected operating system data from a host on a Windows platform +func directIngestOSWindows(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string, failed bool) error { + if failed { + level.Error(logger).Log("op", "directIngestOSWindows", "err", "failed") + return nil + } + if len(rows) != 1 { + return ctxerr.Errorf(ctx, "directIngestOSWindows invalid number of rows: %d", len(rows)) + } + + hostOS := fleet.OperatingSystem{ + Name: rows[0]["name"], + Version: rows[0]["version"], + Arch: rows[0]["arch"], + KernelVersion: rows[0]["kernel_version"], + Platform: rows[0]["platform"], + } + + if err := ds.UpdateHostOperatingSystem(ctx, host.ID, hostOS); err != nil { + return ctxerr.Wrap(ctx, err, "directIngestOSWindows update host operating system") + } + return nil +} + +// directIngestOSUnixLike ingests selected operating system data from a host on a Unix-like platform +// (e.g., darwin or linux operating systems) +func directIngestOSUnixLike(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string, failed bool) error { + if failed { + level.Error(logger).Log("op", "directIngestOSUnixLike", "err", "failed") + return nil + } + if len(rows) != 1 { + return ctxerr.Errorf(ctx, "directIngestOSUnixLike invalid number of rows: %d", len(rows)) + } + name := rows[0]["name"] + version := rows[0]["version"] + major := rows[0]["major"] + minor := rows[0]["minor"] + patch := rows[0]["patch"] + build := rows[0]["build"] + arch := rows[0]["arch"] + kernelVersion := rows[0]["kernel_version"] + platform := rows[0]["platform"] + + hostOS := fleet.OperatingSystem{Name: name, Arch: arch, KernelVersion: kernelVersion, Platform: platform} + hostOS.Version = parseOSVersion(name, version, major, minor, patch, build) + + if err := ds.UpdateHostOperatingSystem(ctx, host.ID, hostOS); err != nil { + return ctxerr.Wrap(ctx, err, "directIngestOSUnixLike update host operating system") + } + return nil +} + +// parseOSVersion returns a point release string for an operating system. Parsing rules +// depend on available data, which varies between operating systems. +func parseOSVersion(name string, version string, major string, minor string, patch string, build string) string { + var osVersion string + if strings.Contains(strings.ToLower(name), "ubuntu") { + // Ubuntu takes a different approach to updating patch IDs so we instead use + // the version string provided after removing the code name. + regx := regexp.MustCompile(`\(.*\)`) + osVersion = strings.TrimSpace(regx.ReplaceAllString(version, "")) + } else if major != "0" || minor != "0" || patch != "0" { + osVersion = fmt.Sprintf("%s.%s.%s", major, minor, patch) + } else { + osVersion = build + } + osVersion = strings.Trim(osVersion, ".") + + return osVersion +} + func directIngestChromeProfiles(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string, failed bool) error { if failed { // assume the extension is not there diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index a478313df0..ddc184f063 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -294,7 +294,7 @@ func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []st func TestGetDetailQueries(t *testing.T) { queriesNoConfig := GetDetailQueries(nil, config.FleetConfig{}) - require.Len(t, queriesNoConfig, 13) + require.Len(t, queriesNoConfig, 15) baseQueries := []string{ "network_interface", "os_version", @@ -309,15 +309,17 @@ func TestGetDetailQueries(t *testing.T) { "google_chrome_profiles", "orbit_info", "battery", + "os_windows", + "os_unix_like", } sortedKeysCompare(t, queriesNoConfig, baseQueries) queriesWithUsers := GetDetailQueries(&fleet.AppConfig{HostSettings: fleet.HostSettings{EnableHostUsers: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}) - require.Len(t, queriesWithUsers, 15) + require.Len(t, queriesWithUsers, 17) sortedKeysCompare(t, queriesWithUsers, append(baseQueries, "users", "scheduled_query_stats")) queriesWithUsersAndSoftware := GetDetailQueries(&fleet.AppConfig{HostSettings: fleet.HostSettings{EnableHostUsers: true, EnableSoftwareInventory: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}) - require.Len(t, queriesWithUsersAndSoftware, 18) + require.Len(t, queriesWithUsersAndSoftware, 20) sortedKeysCompare(t, queriesWithUsersAndSoftware, append(baseQueries, "users", "software_macos", "software_linux", "software_windows", "scheduled_query_stats")) } @@ -489,6 +491,157 @@ func TestDirectIngestBattery(t *testing.T) { require.True(t, ds.ReplaceHostBatteriesFuncInvoked) } +func TestDirectIngestOSWindows(t *testing.T) { + ds := new(mock.Store) + + testHost := fleet.Host{ + ID: 1, + } + testOS := fleet.OperatingSystem{ + Name: "Microsoft Windows 11 Enterprise", + Version: "21H2", + Arch: "64-bit", + KernelVersion: "10.0.22000.795", + } + + ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error { + require.Equal(t, testHost.ID, hostID) + require.Equal(t, testOS, hostOS) + return nil + } + + err := directIngestOSWindows(context.Background(), log.NewNopLogger(), &testHost, ds, []map[string]string{ + {"name": "Microsoft Windows 11 Enterprise", "version": "21H2", "arch": "64-bit", "kernel_version": "10.0.22000.795"}, + }, false) + + require.NoError(t, err) + require.True(t, ds.UpdateHostOperatingSystemFuncInvoked) +} + +func TestDirectIngestOSUnixLike(t *testing.T) { + ds := new(mock.Store) + + for i, tc := range []struct { + data []map[string]string + expected fleet.OperatingSystem + }{ + { + data: []map[string]string{ + { + "name": "macOS", + "version": "12.5", + "major": "12", + "minor": "5", + "patch": "0", + "build": "21G72", + "arch": "x86_64", + "kernel_version": "21.6.0", + }, + }, + expected: fleet.OperatingSystem{ + Name: "macOS", + Version: "12.5.0", + Arch: "x86_64", + KernelVersion: "21.6.0", + }, + }, + { + data: []map[string]string{ + { + "name": "Ubuntu", + "version": "20.04.2 LTS (Focal Fossa)", + "major": "20", + "minor": "4", + "patch": "0", + "build": "", + "arch": "x86_64", + "kernel_version": "5.10.76-linuxkit", + }, + }, + expected: fleet.OperatingSystem{ + Name: "Ubuntu", + Version: "20.04.2 LTS", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + }, + }, + { + data: []map[string]string{ + { + "name": "CentOS Linux", + "version": "CentOS Linux release 7.9.2009 (Core)", + "major": "7", + "minor": "9", + "patch": "2009", + "build": "", + "arch": "x86_64", + "kernel_version": "5.10.76-linuxkit", + }, + }, + expected: fleet.OperatingSystem{ + Name: "CentOS Linux", + Version: "7.9.2009", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + }, + }, + { + data: []map[string]string{ + { + "name": "Debian GNU/Linux", + "version": "10 (buster)", + "major": "10", + "minor": "0", + "patch": "0", + "build": "", + "arch": "x86_64", + "kernel_version": "5.10.76-linuxkit", + }, + }, + expected: fleet.OperatingSystem{ + Name: "Debian GNU/Linux", + Version: "10.0.0", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + }, + }, + { + data: []map[string]string{ + { + "name": "CentOS Linux", + "version": "CentOS Linux release 7.9.2009 (Core)", + "major": "7", + "minor": "9", + "patch": "2009", + "build": "", + "arch": "x86_64", + "kernel_version": "5.10.76-linuxkit", + }, + }, + expected: fleet.OperatingSystem{ + Name: "CentOS Linux", + Version: "7.9.2009", + Arch: "x86_64", + KernelVersion: "5.10.76-linuxkit", + }, + }, + } { + t.Run(tc.expected.Name, func(t *testing.T) { + ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error { + require.Equal(t, uint(i), hostID) + require.Equal(t, tc.expected, hostOS) + return nil + } + + err := directIngestOSUnixLike(context.Background(), log.NewNopLogger(), &fleet.Host{ID: uint(i)}, ds, tc.data, false) + + require.NoError(t, err) + require.True(t, ds.UpdateHostOperatingSystemFuncInvoked) + ds.UpdateHostOperatingSystemFuncInvoked = false + }) + } +} + func TestDangerousReplaceQuery(t *testing.T) { queries := GetDetailQueries(&fleet.AppConfig{HostSettings: fleet.HostSettings{EnableHostUsers: true}}, config.FleetConfig{}) originalQuery := queries["users"].Query