Report mobile devices in "hosts online" (#47222)

This commit is contained in:
Scott Gress
2026-06-24 07:50:35 -07:00
committed by GitHub
parent 82f7405f19
commit 6336443f37
11 changed files with 334 additions and 52 deletions
@@ -0,0 +1 @@
- Enabled tracking of mobile devices for the "hosts online" chart, and added default filtering to that chart that excludes mobile platforms.
@@ -199,6 +199,30 @@ describe("ChartCard", () => {
screen.queryByText(/Data collection is disabled/i)
).not.toBeInTheDocument();
});
it("excludes mobile platforms by default and shows the Filtered badge", async () => {
let requestedPlatforms: string | null = null;
mockServer.use(
http.get(baseUrl("/charts/:metric"), ({ params, request }) => {
requestedPlatforms = new URL(request.url).searchParams.get("platforms");
return HttpResponse.json(
generateMockChartResponse(params.metric as string, 30)
);
})
);
const render = createCustomRenderer({ withBackendMock: true });
render(<ChartCard />);
// The default platform filter is the four non-mobile platforms, which both
// excludes iOS/iPadOS/Android and surfaces the "Filtered" badge on load.
await waitFor(() => {
expect(screen.getByText("Filtered")).toBeInTheDocument();
});
await waitFor(() => {
expect(requestedPlatforms).toBe("darwin,windows,linux,chrome");
});
expect(requestedPlatforms).not.toMatch(/ios|ipados|android/);
});
});
describe("buildInitialChartFilters", () => {
@@ -48,9 +48,17 @@ const baseClass = "chart-card";
// configurable ranges we'll add UI and request-param plumbing for this.
const CHART_DAYS = 30;
// Mobile platforms (iOS, iPadOS, Android) are excluded from the chart by
// default by seeding the platform filter with only the non-mobile platforms.
// This is intentionally hard-coded to this chart for now rather than a general
// per-chart "default filters" config. Because the platform filter is
// inclusion-based, a non-empty default both excludes mobile and makes the
// "Filtered" badge appear on load. Users can opt mobile back in via the filter.
const DEFAULT_CHART_PLATFORMS = ["darwin", "windows", "linux", "chrome"];
const DEFAULT_CHART_FILTERS: IChartFilterState = {
labelIDs: [],
platforms: [],
platforms: DEFAULT_CHART_PLATFORMS,
hostFilterMode: "none",
selectedHosts: [],
softwareFilters: [...ALL_CVE_SOFTWARE_CATEGORY_VALUES],
@@ -238,7 +246,9 @@ const ChartCard = ({
given hour.
<br />
<br />
Currently, only macOS, Windows, Linux, and ChromeOS are supported.
iOS, iPadOS, and Android hosts are excluded by default; include them
in the filter settings. Locked iOS and iPadOS hosts count as online as
long as they have power and an internet connection.
</>
),
tooltipFormatter: ({ value }: { value: number }) =>
@@ -0,0 +1,24 @@
import { PLATFORM_OPTIONS } from "./ChartFilterModal";
describe("ChartFilterModal PLATFORM_OPTIONS", () => {
it("offers mobile platforms (iOS, iPadOS, Android) alongside desktop", () => {
const values = PLATFORM_OPTIONS.map((o) => o.value);
expect(values).toEqual([
"darwin",
"windows",
"linux",
"chrome",
"ios",
"ipados",
"android",
]);
});
it("labels the mobile platforms for display", () => {
const labelFor = (value: string) =>
PLATFORM_OPTIONS.find((o) => o.value === value)?.label;
expect(labelFor("ios")).toBe("iOS");
expect(labelFor("ipados")).toBe("iPadOS");
expect(labelFor("android")).toBe("Android");
});
});
@@ -31,11 +31,16 @@ const baseClass = "chart-filter-modal";
export type ChartFilterTab = "hosts" | "software";
const PLATFORM_OPTIONS = [
// Exported for testing. Mobile platforms (ios/ipados/android) are selectable
// here; the chart excludes them by default via ChartCard's DEFAULT_CHART_PLATFORMS.
export const PLATFORM_OPTIONS = [
{ label: "macOS", value: "darwin" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "ChromeOS", value: "chrome" },
{ label: "iOS", value: "ios" },
{ label: "iPadOS", value: "ipados" },
{ label: "Android", value: "android" },
];
type HostFilterMode = "none" | "include" | "exclude";
@@ -1,2 +1,2 @@
export { default } from "./ChartFilterModal";
export { default, PLATFORM_OPTIONS } from "./ChartFilterModal";
export type { IChartFilterState, ChartFilterTab } from "./ChartFilterModal";
+7 -5
View File
@@ -71,11 +71,13 @@ type Dataset interface {
// method. It is satisfied by the chart internal Datastore, keeping dataset
// implementations decoupled from internals.
type DatasetStore interface {
// FindOnlineHostIDs returns host IDs that are "online right now" per the
// product's standard online predicate (host_seen_times.seen_time within
// the host's own check-in interval). MDM-only mobile devices (iOS,
// iPadOS, Android) are excluded by design — they don't have
// host_seen_times rows. Used by datasets like uptime.
// FindOnlineHostIDs returns host IDs that are "online right now" using a
// platform-specific predicate. Non-mobile (osquery) hosts use the product's
// standard online predicate (host_seen_times.seen_time within the host's own
// check-in interval). Mobile hosts (iOS, iPadOS, Android), which only check
// in via MDM, use their MDM activity signal (nano_enrollments.last_seen_at,
// falling back to detail_updated_at) within a fixed mobile online window.
// Used by datasets like uptime.
FindOnlineHostIDs(ctx context.Context, now time.Time, disabledFleetIDs []uint) ([]uint, error)
// AffectedHostIDsByCVE returns host IDs grouped by CVE, scoped to the given
+59 -13
View File
@@ -23,6 +23,20 @@ import (
// bounded context must not depend on server/fleet — arch test enforced.
const onlineIntervalBufferSeconds = 60
// mobileOnlineWindowSeconds is the window within which a mobile
// (iOS/iPadOS/Android) host's most recent MDM activity signal must fall for it
// to count as online. Mobile MDM devices have no osquery check-in interval
// (distributed_interval/config_tls_refresh are 0), so instead of a per-host
// interval we anchor the window to the iOS/iPadOS refetch cadence (1 hour; see
// ListIOSAndIPadOSToRefetch) plus the same grace buffer used for osquery hosts.
const mobileOnlineWindowSeconds = 3600 + onlineIntervalBufferSeconds
// neverTimestamp mirrors server.NeverTimestamp, the sentinel written to
// detail_updated_at before a host's first full detail refetch. Duplicated
// rather than imported because the chart bounded context must not depend on the
// server package — arch test enforced.
const neverTimestamp = "2000-01-01 00:00:00"
// Datastore is the MySQL implementation of the chart datastore.
type Datastore struct {
primary *sqlx.DB
@@ -72,24 +86,56 @@ func (ds *Datastore) GetHostIDsForFilter(ctx context.Context, hostFilter *types.
return ids, nil
}
// FindOnlineHostIDs returns host IDs that are "online" at `now` per the same
// per-host predicate used by the hosts list status=online filter
// (filterHostsByStatus in server/datastore/mysql/hosts.go): the host has a
// host_seen_times row whose seen_time falls within the host's own check-in
// interval (LEAST of distributed_interval and config_tls_refresh) plus the
// OnlineIntervalBuffer grace period.
// FindOnlineHostIDs returns host IDs that are "online" at `now`, using a
// platform-specific predicate:
//
// Because host_seen_times is updated only by osquery check-ins, MDM-only
// mobile devices (iOS, iPadOS, Android) are currently excluded by design.
// - Non-mobile (osquery-capable) hosts use the same predicate as the hosts
// list status=online filter (filterHostsByStatus in
// server/datastore/mysql/hosts.go): a host_seen_times row whose seen_time
// falls within the host's own check-in interval (LEAST of
// distributed_interval and config_tls_refresh) plus the OnlineIntervalBuffer
// grace period.
// - Mobile hosts (iOS, iPadOS, Android) have no osquery check-in interval, so
// they use their MDM activity signal — the most recent of
// nano_enrollments.last_seen_at (bumped on every MDM check-in, and only
// considered for enabled enrollments since last_seen_at is also bumped when
// an enrollment is disabled on checkout) and
// host_seen_times.seen_time, falling back to detail_updated_at (the
// neverTimestamp sentinel treated as null) — within mobileOnlineWindowSeconds
// of `now`. There is deliberately no created_at fallback: a freshly enrolled
// device that never checked in is not "online".
func (ds *Datastore) FindOnlineHostIDs(ctx context.Context, now time.Time, disabledFleetIDs []uint) ([]uint, error) {
query := fmt.Sprintf(`
SELECT h.id
FROM hosts h
JOIN host_seen_times hst ON h.id = hst.host_id
WHERE DATE_ADD(hst.seen_time,
INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND
) > ?`, onlineIntervalBufferSeconds)
args := []any{now.UTC()}
LEFT JOIN host_seen_times hst ON h.id = hst.host_id
LEFT JOIN nano_enrollments ne ON ne.id = h.uuid
AND ne.enabled = 1
AND ne.type IN ('Device', 'User Enrollment (Device)')
WHERE (
(
h.platform NOT IN ('ios', 'ipados', 'android')
AND hst.seen_time IS NOT NULL
AND DATE_ADD(hst.seen_time,
INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND
) > ?
)
OR
(
h.platform IN ('ios', 'ipados', 'android')
AND DATE_ADD(
COALESCE(
GREATEST(
COALESCE(hst.seen_time, ne.last_seen_at),
COALESCE(ne.last_seen_at, hst.seen_time)
),
NULLIF(h.detail_updated_at, ?)
),
INTERVAL %d SECOND
) > ?
)
)`, onlineIntervalBufferSeconds, mobileOnlineWindowSeconds)
args := []any{now.UTC(), neverTimestamp, now.UTC()}
if len(disabledFleetIDs) > 0 {
query += ` AND (h.team_id IS NULL OR h.team_id NOT IN (?))`
+190 -22
View File
@@ -9,11 +9,12 @@ import (
"github.com/stretchr/testify/require"
)
// TestFindOnlineHostIDs covers the per-host online predicate and the
// TestFindOnlineHostIDs covers the platform-specific online predicate and the
// disabledFleetIDs filter: NULL team_id hosts are always retained, hosts in
// disabled fleets are excluded, hosts whose seen_time falls outside their own
// check-in interval are excluded, and hosts without a host_seen_times row at
// all (mobile devices) are excluded.
// disabled fleets are excluded, non-mobile hosts whose seen_time falls outside
// their own check-in interval are excluded, and mobile hosts are evaluated via
// their MDM activity signal (nano_enrollments.last_seen_at / detail_updated_at)
// rather than host_seen_times.
func TestFindOnlineHostIDs(t *testing.T) {
tdb := testutils.SetupTestDB(t, "chart_mysql")
ds := NewDatastore(tdb.Conns(), tdb.Logger)
@@ -27,7 +28,14 @@ func TestFindOnlineHostIDs(t *testing.T) {
{"MultipleDisabledFleets", testFindOnlineMultipleDisabled},
{"NullTeamHostsAlwaysIncluded", testFindOnlineNullTeamRetained},
{"OfflineHostsExcluded", testFindOnlineOfflineExcluded},
{"MobileHostsWithoutSeenTimeExcluded", testFindOnlineMobileExcluded},
{"NonMobileWithoutSeenTimeExcluded", testFindOnlineNonMobileNoSeenTimeExcluded},
{"AppleMobileOnlineViaNanoLastSeen", testFindOnlineAppleMobileOnline},
{"AppleMobileOfflineWhenNanoStale", testFindOnlineAppleMobileStale},
{"AndroidOnlineViaDetailUpdatedAt", testFindOnlineAndroidOnline},
{"AndroidOfflineWhenDetailNever", testFindOnlineAndroidNever},
{"MobileNeverCheckedInExcluded", testFindOnlineMobileNeverCheckedIn},
{"MobileDisabledEnrollmentExcluded", testFindOnlineMobileDisabledEnrollment},
{"MobileDisabledFleetExcluded", testFindOnlineMobileDisabledFleet},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -37,16 +45,32 @@ func TestFindOnlineHostIDs(t *testing.T) {
}
}
// hostSeed describes one row to insert into hosts (and optionally host_seen_times).
// distributedInterval is in seconds; when 0, the table defaults apply (which means
// the effective online window collapses to fleet.OnlineIntervalBuffer alone).
// When omitSeenTime is true, no host_seen_times row is inserted — simulating an
// MDM-only mobile device.
// hostSeed describes one row to insert into hosts (and optionally
// host_seen_times / nano_enrollments).
//
// distributedInterval is in seconds; when 0, the table defaults apply (which
// means the effective online window collapses to fleet.OnlineIntervalBuffer
// alone). When omitSeenTime is true, no host_seen_times row is inserted —
// simulating an MDM-only mobile device.
//
// platform defaults to "" (treated as a non-mobile/osquery host). Set it to
// "ios", "ipados", or "android" to exercise the mobile predicate. For mobile
// hosts, nanoLastSeen seeds a nano_enrollments row (the Apple MDM check-in
// signal) and detailUpdatedAt overrides hosts.detail_updated_at (the Android
// status-report signal); leave either zero to omit it.
//
// nanoDisabled seeds the nano_enrollments row with enabled = 0 (simulating a
// device that checked out, which also bumps last_seen_at). The default is an
// enabled enrollment.
type hostSeed struct {
teamID uint // 0 means NULL
seenTime time.Time
distributedInterval int
omitSeenTime bool
platform string
nanoLastSeen time.Time
nanoDisabled bool
detailUpdatedAt time.Time
}
// seedHosts inserts a host per entry and returns the auto-assigned host ids in
@@ -74,13 +98,26 @@ func seedHosts(t *testing.T, tdb *testutils.TestDB, entries []hostSeed) []uint {
if e.teamID != 0 {
teamArg = e.teamID
}
// detail_updated_at is set to the sentinel so it never spuriously
// makes a host look freshly active to anyone reading from hosts.
uuid := "uuid-" + itoa(uint(i+1))
// detail_updated_at defaults to the sentinel so it never spuriously
// makes a host look freshly active; a non-zero detailUpdatedAt (the
// Android status-report signal) overrides it.
var detailArg any = neverTimestamp
if !e.detailUpdatedAt.IsZero() {
detailArg = e.detailUpdatedAt
}
// created_at must be a valid timestamp. Mobile seeds omit seenTime, so
// fall back to a recent value — which also exercises that the mobile
// predicate does NOT treat a recent created_at as an online signal.
createdArg := e.seenTime
if createdArg.IsZero() {
createdArg = time.Now().UTC()
}
res, err := tdb.DB.ExecContext(ctx, `
INSERT INTO hosts (osquery_host_id, node_key, uuid, hostname, detail_updated_at, created_at, team_id, distributed_interval, config_tls_refresh)
VALUES (?, ?, ?, ?, '2000-01-01 00:00:00', ?, ?, ?, ?)
`, "ohid-"+itoa(uint(i+1)), "nk-"+itoa(uint(i+1)), "uuid-"+itoa(uint(i+1)),
"host-"+itoa(uint(i+1)), e.seenTime, teamArg, e.distributedInterval, e.distributedInterval)
INSERT INTO hosts (osquery_host_id, node_key, uuid, hostname, platform, detail_updated_at, created_at, team_id, distributed_interval, config_tls_refresh)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "ohid-"+itoa(uint(i+1)), "nk-"+itoa(uint(i+1)), uuid,
"host-"+itoa(uint(i+1)), e.platform, detailArg, createdArg, teamArg, e.distributedInterval, e.distributedInterval)
require.NoError(t, err)
raw, err := res.LastInsertId()
require.NoError(t, err)
@@ -91,6 +128,23 @@ func seedHosts(t *testing.T, tdb *testutils.TestDB, entries []hostSeed) []uint {
hostID, e.seenTime)
require.NoError(t, err)
}
// Seed the Apple MDM check-in signal (nano_enrollments.last_seen_at),
// joined to the host by uuid. nano_enrollments requires a nano_devices
// row via FK, so insert that first.
if !e.nanoLastSeen.IsZero() {
_, err = tdb.DB.ExecContext(ctx,
`INSERT INTO nano_devices (id, authenticate) VALUES (?, ?)`, uuid, "auth")
require.NoError(t, err)
enabled := 1
if e.nanoDisabled {
enabled = 0
}
_, err = tdb.DB.ExecContext(ctx, `
INSERT INTO nano_enrollments (id, device_id, type, topic, push_magic, token_hex, last_seen_at, enabled)
VALUES (?, ?, 'Device', 'topic', 'magic', 'hex', ?, ?)`,
uuid, uuid, e.nanoLastSeen, enabled)
require.NoError(t, err)
}
ids = append(ids, hostID)
}
return ids
@@ -192,18 +246,132 @@ func testFindOnlineOfflineExcluded(t *testing.T, tdb *testutils.TestDB, ds *Data
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
func testFindOnlineMobileExcluded(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
func testFindOnlineNonMobileNoSeenTimeExcluded(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// Host 0 is an osquery host, online. Host 1 has no host_seen_times row —
// representing an MDM-only mobile device — and must be excluded by the
// INNER JOIN regardless of how recently it might have checked in via MDM.
// Host 0 is an osquery host, online. Host 1 is a non-mobile (darwin) host
// with no host_seen_times row — the non-mobile branch requires a seen_time,
// so it's excluded. The mobile branch (which would consult MDM signals)
// must not rescue a non-mobile platform.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, seenTime: onlineSeen(now), distributedInterval: defaultInterval}, // 0: osquery online
{teamID: 1, seenTime: now, omitSeenTime: true}, // 1: mobile (no hst row)
{teamID: 1, seenTime: onlineSeen(now), distributedInterval: defaultInterval, platform: "darwin"}, // 0: osquery online
{teamID: 1, platform: "darwin", omitSeenTime: true, nanoLastSeen: now}, // 1: no hst row, has nano signal but not mobile
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
// mobileRecent / mobileStale are activity-signal timestamps relative to the
// mobile online window (mobileOnlineWindowSeconds ≈ 61 minutes).
func mobileRecent(now time.Time) time.Time { return now.Add(-5 * time.Minute) }
func mobileStale(now time.Time) time.Time { return now.Add(-2 * time.Hour) }
func testFindOnlineAppleMobileOnline(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// iOS and iPadOS hosts with a recent nano_enrollments.last_seen_at and no
// host_seen_times row are online via the MDM signal.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 0: online
{teamID: 1, platform: "ipados", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 1: online
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0], ids[1]}, got)
}
func testFindOnlineAppleMobileStale(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// iOS host whose last MDM check-in is older than the mobile window is offline.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 0: online
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileStale(now)}, // 1: offline (stale)
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
func testFindOnlineAndroidOnline(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// Android has no nano_enrollments row; its signal is detail_updated_at
// (written on status reports). A recent value within the window is online;
// a stale one is offline.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "android", omitSeenTime: true, detailUpdatedAt: mobileRecent(now)}, // 0: online
{teamID: 1, platform: "android", omitSeenTime: true, detailUpdatedAt: mobileStale(now)}, // 1: offline
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
func testFindOnlineAndroidNever(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// Android host whose detail_updated_at is still the NeverTimestamp sentinel
// (no status report yet) and has no nano signal is offline — NULLIF drops
// the sentinel and there is no created_at fallback.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "android", omitSeenTime: true, detailUpdatedAt: mobileRecent(now)}, // 0: online
{teamID: 1, platform: "android", omitSeenTime: true}, // 1: sentinel detail, offline
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
func testFindOnlineMobileNeverCheckedIn(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// A freshly enrolled iOS host with no MDM signal at all (sentinel
// detail_updated_at, no nano row, no seen_time) must NOT be online — the
// mobile predicate deliberately has no created_at fallback.
seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "ios", omitSeenTime: true}, // 0: never checked in
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.Empty(t, got)
}
func testFindOnlineMobileDisabledEnrollment(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// Disabling an enrollment (e.g. on checkout) sets nano_enrollments.enabled = 0
// AND bumps last_seen_at to CURRENT_TIMESTAMP. The predicate only joins
// enabled enrollments, so a device that just checked out must NOT count as
// online even though its last_seen_at is recent.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 0: online (enabled)
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now), nanoDisabled: true}, // 1: offline (disabled, last_seen_at bumped on checkout)
})
got, err := ds.FindOnlineHostIDs(ctx, now, nil)
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[0]}, got)
}
func testFindOnlineMobileDisabledFleet(t *testing.T, tdb *testutils.TestDB, ds *Datastore) {
ctx := t.Context()
now := time.Now().UTC().Truncate(time.Second)
// The disabled-fleet exclusion applies to mobile hosts too: the iOS host in
// fleet 1 is dropped while the NULL-team iOS host is retained.
ids := seedHosts(t, tdb, []hostSeed{
{teamID: 1, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 0: excluded (disabled fleet)
{teamID: 0, platform: "ios", omitSeenTime: true, nanoLastSeen: mobileRecent(now)}, // 1: kept (NULL team)
})
got, err := ds.FindOnlineHostIDs(ctx, now, []uint{1})
require.NoError(t, err)
assert.ElementsMatch(t, []uint{ids[1]}, got)
}
+1 -1
View File
@@ -50,7 +50,7 @@ func (tdb *TestDB) Conns() *common_mysql.DBConnections {
func (tdb *TestDB) TruncateTables(t *testing.T) {
t.Helper()
mysql_testing_utils.TruncateTables(t, tdb.DB, tdb.Logger, nil,
"host_scd_data", "hosts", "host_seen_times", "nano_enrollments", "teams",
"host_scd_data", "hosts", "host_seen_times", "nano_devices", "nano_enrollments", "teams",
"software", "software_cve", "cve_meta", "operating_system_vulnerabilities")
}
+9 -7
View File
@@ -48,13 +48,15 @@ type CVEChartFilter struct {
// Datastore is the internal datastore interface for the chart bounded context.
type Datastore interface {
// FindOnlineHostIDs returns host IDs that are "online right now" per the
// product's standard online predicate: host_seen_times.seen_time falls
// within the host's own check-in interval (LEAST of distributed_interval
// and config_tls_refresh, plus a 60-second grace period that mirrors
// fleet.OnlineIntervalBuffer). Hosts without a host_seen_times row —
// iOS, iPadOS, and Android devices, which only check in via MDM — are
// excluded. Used by datasets like uptime.
// FindOnlineHostIDs returns host IDs that are "online right now" using a
// platform-specific predicate. Non-mobile (osquery) hosts use the product's
// standard online predicate: host_seen_times.seen_time within the host's own
// check-in interval (LEAST of distributed_interval and config_tls_refresh,
// plus a 60-second grace period that mirrors fleet.OnlineIntervalBuffer).
// Mobile hosts (iOS, iPadOS, Android), which only check in via MDM, use
// their MDM activity signal (nano_enrollments.last_seen_at, falling back to
// detail_updated_at) within a fixed mobile online window. Used by datasets
// like uptime.
FindOnlineHostIDs(ctx context.Context, now time.Time, disabledFleetIDs []uint) ([]uint, error)
// AffectedHostIDsByCVE returns host IDs grouped by CVE, scoped to the given