Add Apple marketing names to backend, frontend, and an osquery table (#46482)

**Related issue:** Resolves
https://github.com/fleetdm/fleet/issues/46818 and
https://github.com/fleetdm/fleet/issues/48524.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [x] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified that fleetd runs on macOS, Linux and Windows
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Host lists and Host details now show human‑readable Apple hardware
marketing names (macOS, iOS, iPadOS) where available (e.g., "MacBook Pro
(16‑inch, 2021)"), replacing raw model identifiers.
* Hardware model displays fall back to the original model identifier for
non‑Apple or unmapped devices.

* **Bug Fixes / CSV**
* Exported host CSVs now align with the UI by using the marketing name
for Apple devices when available.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
This commit is contained in:
Steven Palmesano
2026-07-27 22:26:30 -03:00
committed by GitHub
co-authored by Copilot Autofix powered by AI Lucas Manuel Rodriguez
parent 5db4146b4f
commit bf3e1bab99
30 changed files with 1044 additions and 113 deletions
@@ -0,0 +1 @@
- Added marketing name display for Apple devices (macOS, iOS, iPadOS) on the Hosts and Host details pages. The "Hardware model" field now shows human-readable names (e.g. "MacBook Pro (16-inch, 2021)") instead of raw identifiers (e.g. "MacBookPro18,1").
@@ -23,6 +23,7 @@
"gigs_all_disk_space": null,
"gigs_disk_space_available": 0,
"gigs_total_disk_space": 0,
"hardware_marketing_name": "",
"hardware_model": "",
"hardware_serial": "",
"hardware_vendor": "",
@@ -23,6 +23,7 @@ spec:
gigs_all_disk_space: null
gigs_disk_space_available: 0
gigs_total_disk_space: 0
hardware_marketing_name: ""
hardware_model: ""
hardware_serial: ""
hardware_vendor: ""
@@ -30,6 +30,7 @@
"gigs_all_disk_space": null,
"gigs_disk_space_available": 0,
"gigs_total_disk_space": 0,
"hardware_marketing_name": "",
"hardware_model": "",
"hardware_serial": "",
"hardware_vendor": "",
@@ -103,6 +104,7 @@
"gigs_all_disk_space": null,
"gigs_disk_space_available": 0,
"gigs_total_disk_space": 0,
"hardware_marketing_name": "",
"hardware_model": "",
"hardware_serial": "",
"hardware_vendor": "",
@@ -31,6 +31,7 @@
"gigs_all_disk_space": null,
"gigs_disk_space_available": 0,
"gigs_total_disk_space": 0,
"hardware_marketing_name": "",
"hardware_model": "",
"hardware_serial": "",
"hardware_vendor": "",
@@ -104,6 +105,7 @@
"gigs_all_disk_space": null,
"gigs_disk_space_available": 0,
"gigs_total_disk_space": 0,
"hardware_marketing_name": "",
"hardware_model": "",
"hardware_serial": "",
"hardware_vendor": "",
@@ -26,6 +26,7 @@ spec:
gigs_all_disk_space: null
gigs_disk_space_available: 0
gigs_total_disk_space: 0
hardware_marketing_name: ""
hardware_model: ""
hardware_serial: ""
hardware_vendor: ""
@@ -95,6 +96,7 @@ spec:
gigs_all_disk_space: null
gigs_disk_space_available: 0
gigs_total_disk_space: 0
hardware_marketing_name: ""
hardware_model: ""
hardware_serial: ""
hardware_vendor: ""
+1
View File
@@ -64,6 +64,7 @@ const DEFAULT_HOST_MOCK: IHost = {
cpu_logical_cores: 8,
hardware_vendor: "",
hardware_model: "",
hardware_marketing_name: "",
hardware_version: "",
hardware_serial: "",
computer_name: "9b20fc72a247",
@@ -19,6 +19,10 @@ interface ITooltipTruncatedTextCellProps {
prefix?: React.ReactNode;
/** Content does not get truncated */
suffix?: React.ReactNode;
/** When `true`, show the tooltip even when the text is not truncated. Use
* when the tooltip carries supplemental info (e.g. a raw identifier behind a
* friendlier display value) rather than just the truncated text. */
alwaysShowTooltip?: boolean;
}
const baseClass = "tooltip-truncated-cell";
@@ -30,6 +34,7 @@ const TooltipTruncatedTextCell = ({
className,
prefix,
suffix,
alwaysShowTooltip = false,
}: ITooltipTruncatedTextCellProps): JSX.Element => {
const classNames = classnames(baseClass, className, {
"tooltip-break-on-word": tooltipBreakOnWord,
@@ -62,7 +67,9 @@ const TooltipTruncatedTextCell = ({
className="data-table__tooltip-truncated-text-container"
data-tip
data-for={tooltipId}
data-tip-disable={isDefaultValue || tooltipDisabled}
data-tip-disable={
isDefaultValue || (tooltipDisabled && !alwaysShowTooltip)
}
>
<span
ref={ref}
@@ -19,6 +19,10 @@ interface ITooltipTruncatedTextCellProps {
/** When `true`, suppress the tooltip even if the text is truncated. Useful
* when a parent surface owns the hover tooltip. */
disableTooltip?: boolean;
/** When `true`, show the tooltip even when the text is not truncated. Use
* when the tooltip carries supplemental info (e.g. a raw identifier behind a
* friendlier display value) rather than just the truncated text. */
alwaysShowTooltip?: boolean;
}
const baseClass = "tooltip-truncated-text";
@@ -31,18 +35,30 @@ const TooltipTruncatedText = ({
isMobileView = false,
fixedPositionStrategy = false,
disableTooltip = false,
alwaysShowTooltip = false,
}: ITooltipTruncatedTextCellProps): JSX.Element => {
const classNames = classnames(baseClass, className);
// Tooltip visibility logic: Enable only when text is truncated
// Tooltip visibility logic: Enable when text is truncated, or always when
// `alwaysShowTooltip` is set (supplemental info tooltip).
const ref = useRef<HTMLInputElement>(null);
const isTruncated = useCheckTruncatedElement(ref);
const showTooltip = !disableTooltip && (isTruncated || alwaysShowTooltip);
// Underline the value to signal a supplemental tooltip is available.
// Truncation-only tooltips are not underlined.
const underline = showTooltip && alwaysShowTooltip;
const classNames = classnames(baseClass, className, {
[`${baseClass}--underline`]: underline,
});
// TODO: RachelPerkins unreleased bug refactor to include mobile tapping/click
return (
<TooltipWrapper
className={classNames}
disableTooltip={disableTooltip || !isTruncated}
disableTooltip={!showTooltip}
// The underline is applied on the text value via `--underline` (see
// _styles.scss) instead of TooltipWrapper's own underline, whose
// negative margin gets clipped by truncating (`overflow: hidden`) parents.
underline={false}
position={tooltipPosition}
showArrow
@@ -16,4 +16,14 @@
overflow: hidden;
white-space: nowrap;
}
// When a supplemental tooltip is available, underline the value to signal it.
// The dashed border sits on the text value itself (not the wrapper element)
// so it isn't clipped by truncating (`overflow: hidden`) parents such as the
// DataSet `dd`.
&--underline &__text-value {
display: inline-block;
max-width: 100%;
border-bottom: 1px dashed $ui-fleet-black-50;
}
}
+1
View File
@@ -353,6 +353,7 @@ export interface IHost {
cpu_logical_cores: number;
hardware_vendor: string;
hardware_model: string;
hardware_marketing_name: string;
hardware_version: string;
hardware_serial: string;
computer_name: string;
@@ -40,7 +40,7 @@ import {
} from "interfaces/datatable_config";
import PATHS from "router/paths";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { getHostStatusTooltipText } from "../helpers";
import { getHardwareModelDisplay, getHostStatusTooltipText } from "../helpers";
type IHostTableColumnConfig = Column<IHost> & {
// This is used to prevent these columns from being hidden. This will be
@@ -189,9 +189,21 @@ const allHostTableHeaders = (teamId?: number): IHostTableColumnConfig[] => [
),
accessor: "hardware_model",
id: "hardware_model",
Cell: (cellProps: IHostTableStringCellProps) => (
<TooltipTruncatedTextCell value={cellProps.cell.value} className="w250" />
),
Cell: (cellProps: IHostTableStringCellProps) => {
const { value, tooltip, alwaysShowTooltip } = getHardwareModelDisplay(
cellProps.row.original.platform,
cellProps.cell.value,
cellProps.row.original.hardware_marketing_name
);
return (
<TooltipTruncatedTextCell
value={value}
tooltip={tooltip}
alwaysShowTooltip={alwaysShowTooltip}
className="w250"
/>
);
},
},
// User email
{
@@ -1638,10 +1638,14 @@ const ManageHostsPage = ({
.filter((element) => element !== "" && element !== "selection")
// "agent" is a display-only column that coalesces orbit and osquery
// versions; it has no corresponding CSV field on the backend, so we
// substitute the real fields it's derived from.
// substitute the real fields it's derived from. Likewise, the
// "hardware_model" column also surfaces the Apple marketing name in
// the UI, so we export both fields separately.
.reduce((acc: string[], element) => {
if (element === "agent") {
acc.push("orbit_version", "osquery_version");
} else if (element === "hardware_model") {
acc.push("hardware_model", "hardware_marketing_name");
} else {
acc.push(element);
}
@@ -52,7 +52,8 @@ describe("Vitals Card component", () => {
it("renders Enrollment ID and Hardware model for personally enrolled iOS hosts", () => {
const mockHost = createMockHost({
platform: "ios",
hardware_model: "iPhone 12",
hardware_model: "iPhone12,1",
hardware_marketing_name: "iPhone 11",
hardware_serial: "",
uuid: "enrollment-id-12345",
mdm: createMockHostMdmData({
@@ -65,7 +66,7 @@ describe("Vitals Card component", () => {
expect(screen.getByText("Enrollment ID")).toBeInTheDocument();
expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument();
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("iPhone 12")).toBeInTheDocument();
expect(screen.getByText("iPhone 11")).toBeInTheDocument();
expect(screen.queryByText("Serial number")).not.toBeInTheDocument();
expect(screen.queryByText("Private IP address")).not.toBeInTheDocument();
expect(screen.queryByText("Public IP address")).not.toBeInTheDocument();
@@ -74,7 +75,8 @@ describe("Vitals Card component", () => {
it("renders Enrollment ID and Hardware model for personally enrolled iPad hosts", () => {
const mockHost = createMockHost({
platform: "ipados",
hardware_model: "IPad Pro",
hardware_model: "iPad14,5",
hardware_marketing_name: "iPad Pro 12.9-inch (6th generation) Wi-Fi",
hardware_serial: "",
uuid: "enrollment-id-12345",
mdm: createMockHostMdmData({
@@ -87,7 +89,9 @@ describe("Vitals Card component", () => {
expect(screen.getByText("Enrollment ID")).toBeInTheDocument();
expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument();
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("IPad Pro")).toBeInTheDocument();
expect(
screen.getByText("iPad Pro 12.9-inch (6th generation) Wi-Fi")
).toBeInTheDocument();
expect(screen.queryByText("Serial number")).not.toBeInTheDocument();
expect(screen.queryByText("Private IP address")).not.toBeInTheDocument();
expect(screen.queryByText("Public IP address")).not.toBeInTheDocument();
@@ -96,7 +100,8 @@ describe("Vitals Card component", () => {
it("renders Serial number and Hardware model for non-personally enrolled iOS hosts", () => {
const mockHost = createMockHost({
platform: "ios",
hardware_model: "iPhone 12",
hardware_model: "iPhone12,1",
hardware_marketing_name: "iPhone 11",
hardware_serial: "123-456-789",
uuid: "enrollment-id-12345",
mdm: createMockHostMdmData({
@@ -107,7 +112,7 @@ describe("Vitals Card component", () => {
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("iPhone 12")).toBeInTheDocument();
expect(screen.getByText("iPhone 11")).toBeInTheDocument();
expect(screen.getByText("Serial number")).toBeInTheDocument();
expect(screen.getAllByText("123-456-789")[0]).toBeInTheDocument();
expect(screen.queryByText("Enrollment ID")).not.toBeInTheDocument();
@@ -118,7 +123,8 @@ describe("Vitals Card component", () => {
it("renders Enrollment ID and Hardware model for non-personally enrolled iPad hosts", () => {
const mockHost = createMockHost({
platform: "ipados",
hardware_model: "IPad Pro",
hardware_model: "iPad14,5",
hardware_marketing_name: "iPad Pro 12.9-inch (6th generation) Wi-Fi",
hardware_serial: "123-456-789",
uuid: "enrollment-id-12345",
mdm: createMockHostMdmData({
@@ -129,7 +135,9 @@ describe("Vitals Card component", () => {
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("IPad Pro")).toBeInTheDocument();
expect(
screen.getByText("iPad Pro 12.9-inch (6th generation) Wi-Fi")
).toBeInTheDocument();
expect(screen.getByText("Serial number")).toBeInTheDocument();
expect(screen.getAllByText("123-456-789")[0]).toBeInTheDocument();
expect(screen.queryByText("Enrollment ID")).not.toBeInTheDocument();
@@ -140,7 +148,8 @@ describe("Vitals Card component", () => {
it("render Hardware model, IP addresses, and EnrollmentID for all non android and ios/ipad hosts that have enrolled their personal mdm devices", () => {
const mockHost = createMockHost({
platform: "darwin",
hardware_model: "MacBook Pro",
hardware_model: "MacBookPro18,1",
hardware_marketing_name: "MacBook Pro (16-inch, 2021)",
hardware_serial: "",
primary_ip: "192.168.1.1",
public_ip: "203.0.113.1",
@@ -155,7 +164,7 @@ describe("Vitals Card component", () => {
expect(screen.getByText("Enrollment ID")).toBeInTheDocument();
expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument();
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro (16-inch, 2021)")).toBeInTheDocument();
expect(screen.getByText("Private IP address")).toBeInTheDocument();
expect(screen.getAllByText("192.168.1.1")[0]).toBeInTheDocument();
expect(screen.getByText("Public IP address")).toBeInTheDocument();
@@ -166,7 +175,8 @@ describe("Vitals Card component", () => {
it("render Hardware model, IP addresses, and Serial number for all non android and ios/ipad hosts that have enrolled not enrolled in MDM", () => {
const mockHost = createMockHost({
platform: "darwin",
hardware_model: "MacBook Pro",
hardware_model: "MacBookPro18,1",
hardware_marketing_name: "MacBook Pro (16-inch, 2021)",
hardware_serial: "test-serial-number",
primary_ip: "192.168.1.1",
public_ip: "203.0.113.1",
@@ -177,7 +187,7 @@ describe("Vitals Card component", () => {
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro (16-inch, 2021)")).toBeInTheDocument();
expect(screen.getByText("Private IP address")).toBeInTheDocument();
expect(screen.getAllByText("192.168.1.1")[0]).toBeInTheDocument();
expect(screen.getByText("Public IP address")).toBeInTheDocument();
@@ -190,7 +200,8 @@ describe("Vitals Card component", () => {
it("render Hardware model, IP addresses, and Serial number for all non android and ios/ipad hosts that have manually enrolled in MDM", () => {
const mockHost = createMockHost({
platform: "darwin",
hardware_model: "MacBook Pro",
hardware_model: "MacBookPro18,1",
hardware_marketing_name: "MacBook Pro (16-inch, 2021)",
hardware_serial: "test-serial-number",
primary_ip: "192.168.1.1",
public_ip: "203.0.113.1",
@@ -203,7 +214,7 @@ describe("Vitals Card component", () => {
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro (16-inch, 2021)")).toBeInTheDocument();
expect(screen.getByText("Private IP address")).toBeInTheDocument();
expect(screen.getAllByText("192.168.1.1")[0]).toBeInTheDocument();
expect(screen.getByText("Public IP address")).toBeInTheDocument();
@@ -216,7 +227,8 @@ describe("Vitals Card component", () => {
it("render Hardware model, IP addresses, and Serial number for all non android and ios/ipad hosts that have automatically enrolled in MDM", () => {
const mockHost = createMockHost({
platform: "darwin",
hardware_model: "MacBook Pro",
hardware_model: "MacBookPro18,1",
hardware_marketing_name: "MacBook Pro (16-inch, 2021)",
hardware_serial: "test-serial-number",
primary_ip: "192.168.1.1",
public_ip: "203.0.113.1",
@@ -229,7 +241,7 @@ describe("Vitals Card component", () => {
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro (16-inch, 2021)")).toBeInTheDocument();
expect(screen.getByText("Private IP address")).toBeInTheDocument();
expect(screen.getAllByText("192.168.1.1")[0]).toBeInTheDocument();
expect(screen.getByText("Public IP address")).toBeInTheDocument();
@@ -23,6 +23,7 @@ import {
removeOSPrefix,
compareVersions,
} from "utilities/helpers";
import { getHardwareModelDisplay } from "pages/hosts/helpers";
import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
import TooltipWrapper from "components/TooltipWrapper";
@@ -347,13 +348,24 @@ const Vitals = ({
}
// Hardware model
const hardwareModelDisplay = getHardwareModelDisplay(
vitalsData.platform,
vitalsData.hardware_model,
vitalsData.hardware_marketing_name
);
vitals.push({
sortKey: "Hardware model",
element: (
<DataSet
key="hardware-model"
title="Hardware model"
value={<TooltipTruncatedText value={vitalsData.hardware_model} />}
value={
<TooltipTruncatedText
value={hardwareModelDisplay.value}
tooltip={hardwareModelDisplay.tooltip}
alwaysShowTooltip={hardwareModelDisplay.alwaysShowTooltip}
/>
}
/>
),
});
-22
View File
@@ -1,22 +0,0 @@
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
export const getHostStatusTooltipText = (status: string): string => {
if (status === "online") {
return "Online hosts will respond to a live report.";
}
if (status === DEFAULT_EMPTY_CELL_VALUE) {
return "Device is pending enrollment in Apple Business and status is not yet available.";
}
return "Offline hosts won't respond to a live report because they may be shut down, asleep, or not connected to the internet.";
};
export const getHostStatus = (
status: string,
mdmEnrollmentStatus?: string
): string => {
if (mdmEnrollmentStatus === "Pending") {
return DEFAULT_EMPTY_CELL_VALUE;
}
return status || DEFAULT_EMPTY_CELL_VALUE;
};
+65
View File
@@ -0,0 +1,65 @@
import React from "react";
import { isAppleDevice } from "interfaces/platform";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
export const getHostStatusTooltipText = (status: string): string => {
if (status === "online") {
return "Online hosts will respond to a live report.";
}
if (status === DEFAULT_EMPTY_CELL_VALUE) {
return "Device is pending enrollment in Apple Business and status is not yet available.";
}
return "Offline hosts won't respond to a live report because they may be shut down, asleep, or not connected to the internet.";
};
export const getHostStatus = (
status: string,
mdmEnrollmentStatus?: string
): string => {
if (mdmEnrollmentStatus === "Pending") {
return DEFAULT_EMPTY_CELL_VALUE;
}
return status || DEFAULT_EMPTY_CELL_VALUE;
};
// getHardwareModelDisplay computes how a host's hardware model is presented.
// Apple devices with a known marketing name (e.g. "MacBook Pro (16-inch,
// 2021)") show it in place of the raw model and reveal the raw model in a
// tooltip; otherwise the raw model is shown with no supplemental tooltip.
// An empty field may be "" (raw API data) or DEFAULT_EMPTY_CELL_VALUE (data
// that went through normalizeEmptyValues) — both count as "no value".
export const getHardwareModelDisplay = (
platform: string,
hardwareModel: string,
hardwareMarketingName: string
): { value: string; tooltip?: JSX.Element; alwaysShowTooltip: boolean } => {
const isEmpty = (val: string) => !val || val === DEFAULT_EMPTY_CELL_VALUE;
const marketingName =
isAppleDevice(platform) && !isEmpty(hardwareMarketingName)
? hardwareMarketingName
: "";
// Only reveal the raw model on hover when we're actually showing a distinct
// marketing name in its place. When there's no mapping the marketing name is
// empty (or echoes the raw model), so we show the raw model plainly with no
// tooltip.
const showModelTooltip =
!!marketingName &&
!isEmpty(hardwareModel) &&
marketingName !== hardwareModel;
return {
value: marketingName || hardwareModel,
tooltip: showModelTooltip ? (
// Left-align to override the tooltip's default centered text.
<div style={{ textAlign: "left" }}>
<b>Model:</b> {hardwareModel}
<br />
<b>Marketing name:</b> {marketingName}
</div>
) : undefined,
alwaysShowTooltip: showModelTooltip,
};
};
+1
View File
@@ -449,6 +449,7 @@ export const HOST_VITALS_DATA = [
"uptime",
"last_enrolled_at",
"hardware_model",
"hardware_marketing_name",
"hardware_serial",
"primary_ip",
"public_ip",
@@ -0,0 +1 @@
- Added new `apple_hardware_info` osquery extension table (macOS only) with a `marketing_name` column that returns the human-readable marketing name for the current Apple device.
@@ -0,0 +1,46 @@
//go:build darwin
package apple_hardware_info
import (
"context"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
osqclient "github.com/osquery/osquery-go"
"github.com/osquery/osquery-go/plugin/table"
)
// Columns defines the schema for the apple_hardware_info table.
func Columns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.TextColumn("marketing_name"),
}
}
// Generate queries system_info for the hardware model and maps it to its marketing name.
func Generate(ctx context.Context, _ table.QueryContext, socket string) ([]map[string]string, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
c, err := osqclient.NewClient(socket, 2*time.Second)
if err != nil {
return nil, err
}
defer c.Close()
row, err := c.QueryRowContext(ctx, "SELECT hardware_model FROM system_info")
if err != nil {
return nil, err
}
model := row["hardware_model"]
// Return an empty marketing_name when there's no mapping entry so a missing
// mapping can be told apart from the raw model identifier.
name := fleet.AppleHardwareModelsToMarketingNames[model]
return []map[string]string{{
"marketing_name": name,
}}, nil
}
+8
View File
@@ -7,6 +7,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/adobe_plugins"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/app_sso_platform"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/apple_hardware_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/codesign"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
@@ -54,6 +55,13 @@ func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
plugins := []osquery.OsqueryPlugin{
// Fleet tables
adobe_plugins.TablePlugin(log.Logger),
table.NewPlugin(
"apple_hardware_info",
apple_hardware_info.Columns(),
func(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
return apple_hardware_info.Generate(ctx, queryContext, opts.Socket)
},
),
table.NewPlugin("icloud_private_relay", privaterelay.Columns(), privaterelay.Generate),
table.NewPlugin("user_login_settings", user_login_settings.Columns(), user_login_settings.Generate),
table.NewPlugin("pwd_policy", pwd_policy.Columns(), pwd_policy.Generate),
+20
View File
@@ -1157,6 +1157,26 @@
"osqueryRepoUrl": "https://github.com/osquery/osquery/blob/master/specs/windows/appcompat_shims.table",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/new/main/schema?filename=tables%2Fappcompat_shims.yml&value=name%3A%20appcompat_shims%0Adescription%3A%20%7C-%20%23%20(required)%20string%20-%20The%20description%20for%20this%20table.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%23%20Add%20description%20here%0Aexamples%3A%20%7C-%20%23%20(optional)%20string%20-%20An%20example%20query%20for%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown%0A%09%23%20Add%20examples%20here%0Anotes%3A%20%7C-%20%23%20(optional)%20string%20-%20Notes%20about%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown.%0A%09%23%20Add%20notes%20here%0Acolumns%3A%20%23%20(required)%0A%09-%20name%3A%20%23%20(required)%20string%20-%20The%20name%20of%20the%20column%0A%09%20%20description%3A%20%23%20(required)%20string%20-%20The%20column's%20description.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%20%20type%3A%20%23%20(required)%20string%20-%20the%20column's%20data%20type%0A%09%20%20required%3A%20%23%20(required)%20boolean%20-%20whether%20or%20not%20this%20column%20is%20required%20to%20query%20this%20table."
},
{
"name": "apple_hardware_info",
"platforms": [
"darwin"
],
"description": "Maps the Apple hardware model identifier to its marketing name.",
"examples": "Get the marketing name for the current Mac.\n\n```\nSELECT marketing_name FROM apple_hardware_info;\n```\n\nJoin with `system_info` to get both the identifier and the marketing name.\n\n```\nSELECT si.hardware_model, ahi.marketing_name FROM system_info si, apple_hardware_info ahi;\n```",
"columns": [
{
"name": "marketing_name",
"type": "text",
"required": false,
"description": "The Apple marketing name for the hardware, e.g. MacBook Pro (16-inch, Nov 2023)."
}
],
"notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/apple_hardware_info",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apple_hardware_info.yml"
},
{
"name": "apps",
"description": "macOS applications installed in known search paths (e.g., /Applications).",
+24
View File
@@ -0,0 +1,24 @@
name: apple_hardware_info
platforms:
- darwin
description: Maps the Apple hardware model identifier to its marketing name.
examples: |-
Get the marketing name for the current Mac.
```
SELECT marketing_name FROM apple_hardware_info;
```
Join with `system_info` to get both the identifier and the marketing name.
```
SELECT si.hardware_model, ahi.marketing_name FROM system_info si, apple_hardware_info ahi;
```
columns:
- name: marketing_name
type: text
required: false
description: "The Apple marketing name for the hardware, e.g. MacBook Pro (16-inch, Nov 2023)."
notes: |-
This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
+431
View File
@@ -0,0 +1,431 @@
package fleet
// AppleHardwareModelsToMarketingNames' names are primarily sourced from SOFA's
// device identifier data:
// https://github.com/macadmins/sofa/blob/5a4fe284215d666a08d6ea9f66a554c90226b57c/data/models/device_identifiers.json.
//
// Add new entries here as Apple releases new devices.
var AppleHardwareModelsToMarketingNames = map[string]string{
"AppleTV1,1": "Apple TV (1st generation)",
"AppleTV11,1": "Apple TV 4K (2nd generation)",
"AppleTV14,1": "Apple TV 4K (3rd generation)",
"AppleTV2,1": "Apple TV (2nd generation)",
"AppleTV3,1": "Apple TV (3rd generation)",
"AppleTV3,2": "Apple TV (3rd generation, Rev A)",
"AppleTV5,3": "Apple TV HD (4th generation)",
"AppleTV6,2": "Apple TV 4K (1st generation)",
"Mac13,1": "Mac Studio (2022)",
"Mac13,2": "Mac Studio (2022)",
"Mac14,10": "MacBook Pro (16-inch, 2023)",
"Mac14,12": "Mac Mini M2 Pro (2023)",
"Mac14,13": "Mac Studio (2023)",
"Mac14,14": "Mac Studio (2023)",
"Mac14,15": "MacBook Air (15-inch, M2, 2023)",
"Mac14,2": "MacBook Air (M2, 2022)",
"Mac14,3": "Mac Mini M2 (2023)",
"Mac14,5": "MacBook Pro (14-inch, 2023)",
"Mac14,6": "MacBook Pro (16-inch, 2023)",
"Mac14,7": "MacBook Pro (13-inch, M2, 2022)",
"Mac14,8": "Mac Pro (2023)",
"Mac14,9": "MacBook Pro (14-inch, 2023)",
"Mac15,10": "MacBook Pro (14-inch, Nov 2023)",
"Mac15,11": "MacBook Pro (16-inch, Nov 2023)",
"Mac15,12": "MacBook Air (13-inch, M3, 2024)",
"Mac15,13": "MacBook Air (15-inch, M3, 2024)",
"Mac15,14": "Mac Studio (2025)",
"Mac15,3": "MacBook Pro (14-inch, Nov 2023)",
"Mac15,4": "iMac (24-inch, 2023, Two ports)",
"Mac15,5": "iMac (24-inch, 2023, Four ports)",
"Mac15,6": "MacBook Pro (14-inch, Nov 2023)",
"Mac15,7": "MacBook Pro (16-inch, Nov 2023)",
"Mac15,8": "MacBook Pro (14-inch, Nov 2023)",
"Mac15,9": "MacBook Pro (16-inch, Nov 2023)",
"Mac16,1": "MacBook Pro (14-inch, 2024)",
"Mac16,10": "Mac Mini M4 (2024)",
"Mac16,11": "Mac Mini M4 Pro (2024)",
"Mac16,12": "MacBook Air (13-inch, M4, 2025)",
"Mac16,13": "MacBook Air (15-inch, M4, 2025)",
"Mac16,15": "Mac Mini M4 Pro (2024)",
"Mac16,2": "iMac (24-inch, 2024, Two ports)",
"Mac16,3": "iMac (24-inch, 2024, Four ports)",
"Mac16,5": "MacBook Pro (16-inch, 2024)",
"Mac16,6": "MacBook Pro (14-inch, 2024)",
"Mac16,7": "MacBook Pro (16-inch, 2024)",
"Mac16,8": "MacBook Pro (14-inch, 2024)",
"Mac16,9": "Mac Studio (2025)",
"Mac17,2": "MacBook Pro 14-inch (M5)",
"Mac17,3": "MacBook Air (13-inch, M5)",
"Mac17,4": "MacBook Air (15-inch, M5)",
"Mac17,5": "MacBook Neo",
"Mac17,6": "MacBook Pro (16-inch, M5 Max)",
"Mac17,7": "MacBook Pro (14-inch, M5 Max)",
"Mac17,8": "MacBook Pro (16-inch, M5 Pro)",
"Mac17,9": "MacBook Pro (14-inch, M5 Pro)",
"MacBook1,1": "MacBook (13-inch, Mid 2006)",
"MacBook10,1": "MacBook (Retina, 12-inch, 2017)",
"MacBook2,1": "MacBook (13-inch, Late 2006)",
"MacBook3,1": "MacBook (13-inch, Late 2007)",
"MacBook4,1": "MacBook (13-inch, Early 2008)",
"MacBook5,1": "MacBook (13-inch, Aluminum, Late 2008)",
"MacBook5,2": "MacBook (13-inch, Mid 2009)",
"MacBook6,1": "MacBook (13-inch, Late 2009)",
"MacBook7,1": "MacBook (13-inch, Mid 2010)",
"MacBook8,1": "MacBook (Retina, 12-inch, Early 2015)",
"MacBook9,1": "MacBook (Retina, 12-inch, Early 2016)",
"MacBookAir1,1": "MacBook Air (Early 2008)",
"MacBookAir10,1": "MacBook Air (M1, 2020)",
"MacBookAir2,1": "MacBook Air (Mid 2009)",
"MacBookAir3,1": "MacBook Air (11-inch, Late 2010)",
"MacBookAir3,2": "MacBook Air (13-inch, Late 2010)",
"MacBookAir4,1": "MacBook Air (11-inch, Mid 2011)",
"MacBookAir4,2": "MacBook Air (13-inch, Mid 2011)",
"MacBookAir5,1": "MacBook Air (11-inch, Mid 2012)",
"MacBookAir5,2": "MacBook Air (13-inch, Mid 2012)",
"MacBookAir6,1": "MacBook Air (11-inch, Mid 2013) / (11-inch, Early 2014)",
"MacBookAir6,2": "MacBook Air (13-inch, Mid 2013) / (13-inch, Early 2014)",
"MacBookAir7,1": "MacBook Air (11-inch, Early 2015)",
"MacBookAir7,2": "MacBook Air (13-inch, Early 2015) / (13-inch, 2017)",
"MacBookAir8,1": "MacBook Air (Retina, 13-inch, 2018)",
"MacBookAir8,2": "MacBook Air (Retina, 13-inch, 2019)",
"MacBookAir9,1": "MacBook Air (Retina, 13-inch, 2020)",
"MacBookPro1,1": "MacBook Pro (15-inch, Early 2006)",
"MacBookPro1,2": "MacBook Pro (17-inch, Early 2006)",
"MacBookPro10,1": "MacBook Pro (Retina, 15-inch, Mid 2012/Early 2013)",
"MacBookPro10,2": "MacBook Pro (Retina, 13-inch, Late 2012/Early 2013)",
"MacBookPro11,1": "MacBook Pro (Retina, 13-inch, Late 2013/Mid 2014)",
"MacBookPro11,2": "MacBook Pro (Retina, 15-inch, Late 2013/Mid 2014)",
"MacBookPro11,3": "MacBook Pro (Retina, 15-inch, Late 2013/Mid 2014)",
"MacBookPro11,4": "MacBook Pro (Retina, 15-inch, Mid 2015)",
"MacBookPro11,5": "MacBook Pro (Retina, 15-inch, Mid 2015)",
"MacBookPro12,1": "MacBook Pro (Retina, 13-inch, Early 2015)",
"MacBookPro13,1": "MacBook Pro (13-inch, 2016)",
"MacBookPro13,2": "MacBook Pro (13-inch, 2016)",
"MacBookPro13,3": "MacBook Pro (15-inch, 2016)",
"MacBookPro14,1": "MacBook Pro (13-inch, 2017)",
"MacBookPro14,2": "MacBook Pro (13-inch, 2017)",
"MacBookPro14,3": "MacBook Pro (15-inch, 2017)",
"MacBookPro15,1": "MacBook Pro (15-inch, 2018/2019)",
"MacBookPro15,2": "MacBook Pro (13-inch, 2018/2019)",
"MacBookPro15,3": "MacBook Pro (15-inch, 2019)",
"MacBookPro15,4": "MacBook Pro (13-inch, 2019)",
"MacBookPro16,1": "MacBook Pro (16-inch, 2019)",
"MacBookPro16,2": "MacBook Pro (13-inch, 2020)",
"MacBookPro16,3": "MacBook Pro (13-inch, 2020)",
"MacBookPro16,4": "MacBook Pro (16-inch, 2019)",
"MacBookPro17,1": "MacBook Pro (13-inch, M1, 2020)",
"MacBookPro18,1": "MacBook Pro (16-inch, 2021)",
"MacBookPro18,2": "MacBook Pro (16-inch, 2021)",
"MacBookPro18,3": "MacBook Pro (14-inch, 2021)",
"MacBookPro18,4": "MacBook Pro (14-inch, 2021)",
"MacBookPro2,1": "MacBook Pro (17-inch, Late 2006)",
"MacBookPro2,2": "MacBook Pro (15-inch, Late 2006)",
"MacBookPro3,1": "MacBook Pro (15-inch, Mid 2007)",
"MacBookPro4,1": "MacBook Pro (Early 2008)",
"MacBookPro5,1": "MacBook Pro (Late 2008)",
"MacBookPro5,2": "MacBook Pro (Early/Mid 2009)",
"MacBookPro5,3": "MacBook Pro (Mid 2009)",
"MacBookPro5,4": "MacBook Pro (15-inch, Mid 2009, Integrated Graphics)",
"MacBookPro5,5": "MacBook Pro (13-inch, Mid 2009)",
"MacBookPro6,1": "MacBook Pro (17-inch, Mid 2010)",
"MacBookPro6,2": "MacBook Pro (15-inch, Mid 2010)",
"MacBookPro7,1": "MacBook Pro (13-inch, Mid 2010)",
"MacBookPro8,1": "MacBook Pro (13-inch, Early/Late 2011)",
"MacBookPro8,2": "MacBook Pro (15-inch, Early/Late 2011)",
"MacBookPro8,3": "MacBook Pro (17-inch, Early/Late 2011)",
"MacBookPro9,1": "MacBook Pro (15-inch, Mid 2012)",
"MacBookPro9,2": "MacBook Pro (13-inch, Mid 2012)",
"MacPro1,1": "Mac Pro (Mid 2006)",
"MacPro2,1": "Mac Pro (Early 2007)",
"MacPro3,1": "Mac Pro (Early 2008)",
"MacPro4,1": "Mac Pro (Early 2009)",
"MacPro5,1": "Mac Pro (Mid 2010/Mid 2012)",
"MacPro6,1": "Mac Pro (Late 2013)",
"MacPro7,1": "Mac Pro (2019)",
"Macmini1,1": "Mac Mini (2006)",
"Macmini2,1": "Mac Mini (2007)",
"Macmini3,1": "Mac Mini (2009)",
"Macmini4,1": "Mac Mini (Mid 2010)",
"Macmini5,1": "Mac Mini (Mid 2011)",
"Macmini5,2": "Mac Mini (Mid 2011)",
"Macmini5,3": "Mac Mini (Mid 2011)",
"Macmini6,1": "Mac Mini (Late 2012)",
"Macmini6,2": "Mac Mini (Late 2012)",
"Macmini7,1": "Mac Mini (Late 2014)",
"Macmini8,1": "Mac Mini (2018)",
"Macmini9,1": "Mac Mini M1 (2020)",
"RealityDevice14,1": "Vision Pro",
"RealityDevice17,1": "Vision Pro (M5)",
"VirtualMac1,1": "Apple Virtual Machine",
"VirtualMac2,1": "Apple Virtual Machine",
"VirtualMac2,3": "Apple Virtual Machine",
"Watch1,1": "Apple Watch (1st generation) (38mm)",
"Watch1,2": "Apple Watch (1st generation) (42mm)",
"Watch2,3": "Apple Watch Series 2 (38mm)",
"Watch2,4": "Apple Watch Series 2 (42mm)",
"Watch2,6": "Apple Watch Series 1 (38mm)",
"Watch2,7": "Apple Watch Series 1 (42mm)",
"Watch3,1": "Apple Watch Series 3 (GPS + Cellular, 38mm)",
"Watch3,2": "Apple Watch Series 3 (GPS + Cellular, 42mm)",
"Watch3,3": "Apple Watch Series 3 (GPS, 38mm)",
"Watch3,4": "Apple Watch Series 3 (GPS, 42mm)",
"Watch4,1": "Apple Watch Series 4 (GPS, 40mm)",
"Watch4,2": "Apple Watch Series 4 (GPS, 44mm)",
"Watch4,3": "Apple Watch Series 4 (GPS + Cellular, 40mm)",
"Watch4,4": "Apple Watch Series 4 (GPS + Cellular, 44mm)",
"Watch5,1": "Apple Watch Series 5 (GPS, 40mm)",
"Watch5,10": "Apple Watch SE (1st generation, GPS, 44mm)",
"Watch5,11": "Apple Watch SE (1st generation, GPS + Cellular, 40mm)",
"Watch5,12": "Apple Watch SE (1st generation, GPS + Cellular, 44mm)",
"Watch5,2": "Apple Watch Series 5 (GPS, 44mm)",
"Watch5,3": "Apple Watch Series 5 (GPS + Cellular, 40mm)",
"Watch5,4": "Apple Watch Series 5 (GPS + Cellular, 44mm)",
"Watch5,9": "Apple Watch SE (1st generation, GPS, 40mm)",
"Watch6,1": "Apple Watch Series 6 (GPS, 40mm)",
"Watch6,10": "Apple Watch SE (2nd generation, GPS, 40mm)",
"Watch6,11": "Apple Watch SE (2nd generation, GPS, 44mm)",
"Watch6,12": "Apple Watch SE (2nd generation, GPS + Cellular, 40mm)",
"Watch6,13": "Apple Watch SE (2nd generation, GPS + Cellular, 44mm)",
"Watch6,14": "Apple Watch Series 8 (GPS, 45mm)",
"Watch6,15": "Apple Watch Series 8 (GPS, 41mm)",
"Watch6,16": "Apple Watch Series 8 (GPS + Cellular, 41mm)",
"Watch6,17": "Apple Watch Series 8 (GPS + Cellular, 45mm)",
"Watch6,18": "Apple Watch Ultra",
"Watch6,2": "Apple Watch Series 6 (GPS, 44mm)",
"Watch6,3": "Apple Watch Series 6 (GPS + Cellular, 40mm)",
"Watch6,4": "Apple Watch Series 6 (GPS + Cellular, 44mm)",
"Watch6,6": "Apple Watch Series 7 (GPS, 41mm)",
"Watch6,7": "Apple Watch Series 7 (GPS, 45mm)",
"Watch6,8": "Apple Watch Series 7 (GPS + Cellular, 41mm)",
"Watch6,9": "Apple Watch Series 7 (GPS + Cellular, 45mm)",
"Watch7,1": "Apple Watch Series 9 (GPS, 41mm)",
"Watch7,10": "Apple Watch Series 10 (GPS + Cellular, 42mm)",
"Watch7,11": "Apple Watch Series 10 (GPS + Cellular, 46mm)",
"Watch7,12": "Apple Watch Ultra 3",
"Watch7,13": "Apple Watch SE 3 (GPS, 40mm)",
"Watch7,14": "Apple Watch SE 3 (GPS, 44mm)",
"Watch7,15": "Apple Watch SE 3 (GPS + Cellular, 40mm)",
"Watch7,16": "Apple Watch SE 3 (GPS + Cellular, 44mm)",
"Watch7,17": "Apple Watch Series 11 (GPS, 42mm)",
"Watch7,18": "Apple Watch Series 11 (GPS, 46mm)",
"Watch7,19": "Apple Watch Series 11 (GPS + Cellular, 42mm)",
"Watch7,2": "Apple Watch Series 9 (GPS, 45mm)",
"Watch7,20": "Apple Watch Series 11 (GPS + Cellular, 46mm)",
"Watch7,3": "Apple Watch Series 9 (GPS + Cellular, 41mm)",
"Watch7,4": "Apple Watch Series 9 (GPS + Cellular, 45mm)",
"Watch7,5": "Apple Watch Ultra 2",
"Watch7,8": "Apple Watch Series 10 (GPS, 42mm)",
"Watch7,9": "Apple Watch Series 10 (GPS, 46mm)",
"Xserve1,1": "Xserve (Late 2006)",
"Xserve2,1": "Xserve (Early 2008)",
"Xserve3,1": "Xserve (Early 2009)",
"iMac10,1": "iMac (21.5-inch, Late 2009)",
"iMac11,1": "iMac (27-inch, Late 2009, Core i5/i7)",
"iMac11,2": "iMac (21.5-inch, Mid 2010)",
"iMac11,3": "iMac (27-inch, Mid 2010)",
"iMac12,1": "iMac (21.5-inch, Mid 2011)",
"iMac12,2": "iMac (27-inch, Mid 2011)",
"iMac13,1": "iMac (21.5-inch, Late 2012)",
"iMac13,2": "iMac (27-inch, Late 2012)",
"iMac13,3": "iMac (21.5-inch, Early 2013)",
"iMac14,1": "iMac (21.5-inch, Late 2013, Integrated Graphics)",
"iMac14,2": "iMac (27-inch, Late 2013)",
"iMac14,3": "iMac (21.5-inch, Late 2013, Dedicated Graphics)",
"iMac14,4": "iMac (21.5-inch, Mid 2014)",
"iMac15,1": "iMac (Retina 5K, 27-inch, Mid 2015)",
"iMac16,1": "iMac (21.5-inch, Late 2015)",
"iMac16,2": "iMac (21.5-inch, Late 2015)",
"iMac17,1": "iMac (Retina 5K, 27-inch, Late 2015)",
"iMac18,1": "iMac (21.5-inch, 2017)",
"iMac18,2": "iMac (Retina 4K, 21.5-inch, 2017)",
"iMac18,3": "iMac (Retina 5K, 27-inch, 2017)",
"iMac19,1": "iMac (Retina 5K, 27-inch, 2019)",
"iMac19,2": "iMac (Retina 4K, 21.5-inch, 2019)",
"iMac20,1": "iMac (Retina 5K, 27-inch, 2020)",
"iMac20,2": "iMac (Retina 5K, 27-inch, 2020, RX 5700/XT)",
"iMac21,1": "iMac (24-inch, M1, 2021)",
"iMac21,2": "iMac (24-inch, M1, 2021)",
"iMac4,1": "iMac (17-inch, Early 2006)",
"iMac4,2": "iMac (17-inch, Mid 2006)",
"iMac5,1": "iMac (17-inch, Late 2006, Dedicated Graphics)",
"iMac5,2": "iMac (17-inch, Late 2006, Integrated Graphics)",
"iMac6,1": "iMac (24-inch, Late 2006)",
"iMac7,1": "iMac (24-inch, Mid 2007)",
"iMac8,1": "iMac (24-inch, Early 2008)",
"iMac9,1": "iMac (20-inch, Mid 2009)",
"iMacPro1,1": "iMac Pro (Retina 5K, 27-inch, Late 2017)",
"iPad1,1": "iPad",
"iPad11,1": "iPad mini (5th generation) Wi-Fi",
"iPad11,2": "iPad mini (5th generation) Wi-Fi + Cellular",
"iPad11,3": "iPad Air (3rd generation) Wi-Fi",
"iPad11,4": "iPad Air (3rd generation) Wi-Fi + Cellular",
"iPad11,6": "iPad (8th generation) Wi-Fi",
"iPad11,7": "iPad (8th generation) Wi-Fi + Cellular",
"iPad12,1": "iPad (9th generation) Wi-Fi",
"iPad12,2": "iPad (9th generation) Wi-Fi + Cellular",
"iPad13,1": "iPad Air (4th generation) Wi-Fi",
"iPad13,10": "iPad Pro 12.9-inch (5th generation) Wi-Fi + Cellular",
"iPad13,11": "iPad Pro 12.9-inch (5th generation) Wi-Fi + Cellular (1 or 2 TB)",
"iPad13,16": "iPad Air (5th generation) Wi-Fi",
"iPad13,17": "iPad Air (5th generation) Wi-Fi + Cellular",
"iPad13,18": "iPad (10th generation) Wi-Fi",
"iPad13,19": "iPad (10th generation) Wi-Fi + Cellular",
"iPad13,2": "iPad Air (4th generation) Wi-Fi + Cellular",
"iPad13,4": "iPad Pro 11-inch (3rd generation) Wi-Fi",
"iPad13,5": "iPad Pro 11-inch (3rd generation) Wi-Fi (1 or 2 TB)",
"iPad13,6": "iPad Pro 11-inch (3rd generation) Wi-Fi + Cellular",
"iPad13,7": "iPad Pro 11-inch (3rd generation) Wi-Fi + Cellular (1 or 2 TB)",
"iPad13,8": "iPad Pro 12.9-inch (5th generation) Wi-Fi",
"iPad13,9": "iPad Pro 12.9-inch (5th generation) Wi-Fi (1 or 2 TB)",
"iPad14,1": "iPad mini (6th generation) Wi-Fi",
"iPad14,10": "iPad Air 13-inch (M2) Wi-Fi",
"iPad14,11": "iPad Air 13-inch (M2) Wi-Fi + Cellular",
"iPad14,2": "iPad mini (6th generation) Wi-Fi + Cellular",
"iPad14,3": "iPad Pro 11-inch (4th generation) Wi-Fi",
"iPad14,4": "iPad Pro 11-inch (4th generation) Wi-Fi + Cellular",
"iPad14,5": "iPad Pro 12.9-inch (6th generation) Wi-Fi",
"iPad14,6": "iPad Pro 12.9-inch (6th generation) Wi-Fi + Cellular",
"iPad14,8": "iPad Air 11-inch (M2) Wi-Fi",
"iPad14,9": "iPad Air 11-inch (M2) Wi-Fi + Cellular",
"iPad15,3": "iPad Air 11-inch (M3) Wi-Fi",
"iPad15,4": "iPad Air 11-inch (M3) Wi-Fi + Cellular",
"iPad15,5": "iPad Air 13-inch (M3) Wi-Fi",
"iPad15,6": "iPad Air 13-inch (M3) Wi-Fi + Cellular",
"iPad15,7": "iPad (A16) Wi-Fi",
"iPad15,8": "iPad (A16) Wi-Fi + Cellular",
"iPad16,1": "iPad mini (A17 Pro) Wi-Fi",
"iPad16,10": "iPad Air 13-inch (M4) Wi-Fi",
"iPad16,11": "iPad Air 13-inch (M4) Wi-Fi + Cellular",
"iPad16,2": "iPad mini (A17 Pro) Wi-Fi + Cellular",
"iPad16,3": "iPad Pro 11-inch (M4) Wi-Fi",
"iPad16,4": "iPad Pro 11-inch (M4) Wi-Fi + Cellular",
"iPad16,5": "iPad Pro 13-inch (M4) Wi-Fi",
"iPad16,6": "iPad Pro 13-inch (M4) Wi-Fi + Cellular",
"iPad16,8": "iPad Air 11-inch (M4) Wi-Fi",
"iPad16,9": "iPad Air 11-inch (M4) Wi-Fi + Cellular",
"iPad17,1": "iPad Pro 11-inch Wi-Fi (M5)",
"iPad17,2": "iPad Pro 11-inch Wi-Fi + Cellular (M5)",
"iPad17,3": "iPad Pro 13-inch Wi-Fi (M5)",
"iPad17,4": "iPad Pro 13-inch Wi-Fi + Cellular (M5)",
"iPad2,1": "iPad 2 Wi-Fi",
"iPad2,2": "iPad 2 Wi-Fi + 3G (GSM)",
"iPad2,3": "iPad 2 Wi-Fi + 3G (CDMA)",
"iPad2,4": "iPad 2 Wi-Fi (Mid 2012)",
"iPad2,5": "iPad mini Wi-Fi",
"iPad2,6": "iPad mini Wi-Fi + Cellular",
"iPad2,7": "iPad mini Wi-Fi + Cellular (MM)",
"iPad3,1": "iPad (3rd generation) Wi-Fi",
"iPad3,2": "iPad (3rd generation) Wi-Fi + Cellular (VZ)",
"iPad3,3": "iPad (3rd generation) Wi-Fi + Cellular",
"iPad3,4": "iPad (4th generation) Wi-Fi",
"iPad3,5": "iPad (4th generation) Wi-Fi + Cellular",
"iPad3,6": "iPad (4th generation) Wi-Fi + Cellular (MM)",
"iPad4,1": "iPad Air Wi-Fi",
"iPad4,2": "iPad Air Wi-Fi + Cellular (GSM/CDMA)",
"iPad4,3": "iPad Air Wi-Fi + Cellular (TD-LTE)",
"iPad4,4": "iPad mini 2 Wi-Fi",
"iPad4,5": "iPad mini 2 Wi-Fi + Cellular",
"iPad4,6": "iPad mini 2 Wi-Fi + Cellular (TD-LTE)",
"iPad4,7": "iPad mini 3 Wi-Fi",
"iPad4,8": "iPad mini 3 Wi-Fi + Cellular",
"iPad4,9": "iPad mini 3 Wi-Fi + Cellular (TD-LTE)",
"iPad5,1": "iPad mini 4 Wi-Fi",
"iPad5,2": "iPad mini 4 Wi-Fi + Cellular",
"iPad5,3": "iPad Air 2 Wi-Fi",
"iPad5,4": "iPad Air 2 Wi-Fi + Cellular",
"iPad6,11": "iPad (5th generation) Wi-Fi",
"iPad6,12": "iPad (5th generation) Wi-Fi + Cellular",
"iPad6,3": "iPad Pro (9.7-inch) Wi-Fi",
"iPad6,4": "iPad Pro (9.7-inch) Wi-Fi + Cellular",
"iPad6,7": "iPad Pro (12.9-inch) (1st generation) Wi-Fi",
"iPad6,8": "iPad Pro (12.9-inch) (1st generation) Wi-Fi + Cellular",
"iPad7,1": "iPad Pro 12.9-inch (2nd generation) Wi-Fi",
"iPad7,11": "iPad (7th generation) Wi-Fi",
"iPad7,12": "iPad (7th generation) Wi-Fi + Cellular",
"iPad7,2": "iPad Pro 12.9-inch (2nd generation) Wi-Fi + Cellular",
"iPad7,3": "iPad Pro (10.5-inch) Wi-Fi",
"iPad7,4": "iPad Pro (10.5-inch) Wi-Fi + Cellular",
"iPad7,5": "iPad (6th generation) Wi-Fi",
"iPad7,6": "iPad (6th generation) Wi-Fi + Cellular",
"iPad8,1": "iPad Pro 11-inch (1st generation) Wi-Fi",
"iPad8,10": "iPad Pro 11-inch (2nd generation) Wi-Fi + Cellular",
"iPad8,11": "iPad Pro 12.9-inch (4th generation) Wi-Fi",
"iPad8,12": "iPad Pro 12.9-inch (4th generation) Wi-Fi + Cellular",
"iPad8,2": "iPad Pro 11-inch (1st generation) Wi-Fi (1TB)",
"iPad8,3": "iPad Pro 11-inch (1st generation) Wi-Fi + Cellular",
"iPad8,4": "iPad Pro 11-inch (1st generation) Wi-Fi + Cellular (1TB)",
"iPad8,5": "iPad Pro 12.9-inch (3rd generation) Wi-Fi",
"iPad8,6": "iPad Pro 12.9-inch (3rd generation) Wi-Fi (1TB)",
"iPad8,7": "iPad Pro 12.9-inch (3rd generation) Wi-Fi + Cellular",
"iPad8,8": "iPad Pro 12.9-inch (3rd generation) Wi-Fi + Cellular (1TB)",
"iPad8,9": "iPad Pro 11-inch (2nd generation) Wi-Fi",
"iPhone1,1": "iPhone",
"iPhone1,2": "iPhone 3G",
"iPhone10,1": "iPhone 8 (CDMA)",
"iPhone10,2": "iPhone 8 Plus (CDMA)",
"iPhone10,3": "iPhone X (CDMA)",
"iPhone10,4": "iPhone 8 (GSM)",
"iPhone10,5": "iPhone 8 Plus (GSM)",
"iPhone10,6": "iPhone X (GSM)",
"iPhone11,2": "iPhone XS",
"iPhone11,4": "iPhone XS Max (China mainland)",
"iPhone11,6": "iPhone XS Max",
"iPhone11,8": "iPhone XR",
"iPhone12,1": "iPhone 11",
"iPhone12,3": "iPhone 11 Pro",
"iPhone12,5": "iPhone 11 Pro Max",
"iPhone12,8": "iPhone SE (2nd generation)",
"iPhone13,1": "iPhone 12 mini",
"iPhone13,2": "iPhone 12",
"iPhone13,3": "iPhone 12 Pro",
"iPhone13,4": "iPhone 12 Pro Max",
"iPhone14,2": "iPhone 13 Pro",
"iPhone14,3": "iPhone 13 Pro Max",
"iPhone14,4": "iPhone 13 mini",
"iPhone14,5": "iPhone 13",
"iPhone14,6": "iPhone SE (3rd generation)",
"iPhone14,7": "iPhone 14",
"iPhone14,8": "iPhone 14 Plus",
"iPhone15,2": "iPhone 14 Pro",
"iPhone15,3": "iPhone 14 Pro Max",
"iPhone15,4": "iPhone 15",
"iPhone15,5": "iPhone 15 Plus",
"iPhone16,1": "iPhone 15 Pro",
"iPhone16,2": "iPhone 15 Pro Max",
"iPhone17,1": "iPhone 16 Pro",
"iPhone17,2": "iPhone 16 Pro Max",
"iPhone17,3": "iPhone 16",
"iPhone17,4": "iPhone 16 Plus",
"iPhone17,5": "iPhone 16e",
"iPhone18,1": "iPhone 17 Pro",
"iPhone18,2": "iPhone 17 Pro Max",
"iPhone18,3": "iPhone 17",
"iPhone18,4": "iPhone Air",
"iPhone18,5": "iPhone 17e",
"iPhone2,1": "iPhone 3GS",
"iPhone3,1": "iPhone 4 (GSM)",
"iPhone3,2": "iPhone 4 (GSM, 2012)",
"iPhone3,3": "iPhone 4 (CDMA)",
"iPhone4,1": "iPhone 4s",
"iPhone5,1": "iPhone 5 (GSM)",
"iPhone5,2": "iPhone 5 (CDMA)",
"iPhone5,3": "iPhone 5c (GSM)",
"iPhone5,4": "iPhone 5c (CDMA)",
"iPhone6,1": "iPhone 5s (GSM)",
"iPhone6,2": "iPhone 5s (CDMA)",
"iPhone7,1": "iPhone 6 Plus",
"iPhone7,2": "iPhone 6",
"iPhone8,1": "iPhone 6s",
"iPhone8,2": "iPhone 6s Plus",
"iPhone8,4": "iPhone SE (1st generation)",
"iPhone9,1": "iPhone 7 (CDMA)",
"iPhone9,2": "iPhone 7 Plus (CDMA)",
"iPhone9,3": "iPhone 7 (GSM)",
"iPhone9,4": "iPhone 7 Plus (GSM)",
"iPod1,1": "iPod touch (1st generation)",
"iPod2,1": "iPod touch (2nd generation)",
"iPod3,1": "iPod touch (3rd generation)",
"iPod4,1": "iPod touch (4th generation)",
"iPod5,1": "iPod touch (5th generation)",
"iPod7,1": "iPod touch (6th generation)",
"iPod9,1": "iPod touch (7th generation)",
}
+19 -16
View File
@@ -12,14 +12,15 @@ type HostResponse struct {
*Host
// Add alias fields for team name and ID for use in CSV reports.
// TODO: clean up in Fleet 5.
FleetID *uint `json:"-" csv:"fleet_id"`
FleetName *string `json:"-" csv:"fleet_name"`
Status HostStatus `json:"status" csv:"status"`
DisplayText string `json:"display_text" csv:"display_text"`
DisplayName string `json:"display_name" csv:"display_name"`
Labels []*Label `json:"labels,omitempty" csv:"-"`
Geolocation *GeoLocation `json:"geolocation,omitempty" csv:"-"`
CSVDeviceMapping string `json:"-" db:"-" csv:"device_mapping"`
FleetID *uint `json:"-" csv:"fleet_id"`
FleetName *string `json:"-" csv:"fleet_name"`
Status HostStatus `json:"status" csv:"status"`
DisplayText string `json:"display_text" csv:"display_text"`
DisplayName string `json:"display_name" csv:"display_name"`
Labels []*Label `json:"labels,omitempty" csv:"-"`
Geolocation *GeoLocation `json:"geolocation,omitempty" csv:"-"`
CSVDeviceMapping string `json:"-" db:"-" csv:"device_mapping"`
HardwareMarketingName string `json:"hardware_marketing_name" csv:"hardware_marketing_name"`
}
// HostResponseForHost returns a HostResponse from Host with Geolocation.
@@ -32,10 +33,11 @@ func HostResponseForHost(ctx context.Context, svc Service, host *Host) *HostResp
// HostResponseForHostCheap returns a new HostResponse from a Host without computing Geolocation.
func HostResponseForHostCheap(host *Host) *HostResponse {
return &HostResponse{
Host: host,
Status: host.Status(time.Now()),
DisplayText: host.Hostname,
DisplayName: host.DisplayName(),
Host: host,
Status: host.Status(time.Now()),
DisplayText: host.Hostname,
DisplayName: host.DisplayName(),
HardwareMarketingName: host.HardwareMarketingName(),
}
}
@@ -53,8 +55,9 @@ func HostResponsesForHostsCheap(hosts []Host) []HostResponse {
// with the HostDetail details.
type HostDetailResponse struct {
HostDetail
Status HostStatus `json:"status"`
DisplayText string `json:"display_text"`
DisplayName string `json:"display_name"`
Geolocation *GeoLocation `json:"geolocation,omitempty"`
Status HostStatus `json:"status"`
DisplayText string `json:"display_text"`
DisplayName string `json:"display_name"`
Geolocation *GeoLocation `json:"geolocation,omitempty"`
HardwareMarketingName string `json:"hardware_marketing_name"`
}
+13
View File
@@ -1082,6 +1082,19 @@ func (h *Host) DisplayName() string {
return HostDisplayName(h.ComputerName, h.Hostname, h.HardwareModel, h.HardwareSerial)
}
// HardwareMarketingName returns the Apple marketing name for the host's hardware
// model (e.g. "MacBook Pro (16-inch, Nov 2023)"). It returns an empty string
// when the platform is not an Apple platform or the identifier is not in the
// mapping, so a missing mapping entry can be told apart from the raw model.
func (h *Host) HardwareMarketingName() string {
if IsApplePlatform(h.Platform) {
if name, ok := AppleHardwareModelsToMarketingNames[h.HardwareModel]; ok {
return name
}
}
return ""
}
func (h *HostLite) DisplayName() string {
return HostDisplayName(h.ComputerName, h.Hostname, h.HardwareModel, h.HardwareSerial)
}
+94 -41
View File
@@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"iter"
"net/http"
"reflect"
@@ -74,11 +73,12 @@ func hostDetailResponseForHost(ctx context.Context, svc fleet.Service, host *fle
}
return &fleet.HostDetailResponse{
HostDetail: *host,
Status: host.Status(time.Now()),
DisplayText: host.Hostname,
DisplayName: host.DisplayName(),
Geolocation: geoLoc,
HostDetail: *host,
Status: host.Status(time.Now()),
DisplayText: host.Hostname,
DisplayName: host.DisplayName(),
Geolocation: geoLoc,
HardwareMarketingName: host.HardwareMarketingName(),
}, nil
}
@@ -3089,40 +3089,41 @@ func (r hostsReportResponse) HijackRender(ctx context.Context, w http.ResponseWr
return
}
// read back the CSV to reorder and (optionally) filter columns
recs, err := csv.NewReader(&buf).ReadAll()
if err != nil {
logging.WithErr(ctx, err)
encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w)
return
}
returnAll := len(r.Columns) == 0
var outRows [][]string
if !returnAll {
// read back the CSV to filter out any unwanted columns
recs, err := csv.NewReader(&buf).ReadAll()
if err != nil {
logging.WithErr(ctx, err)
encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w)
return
if returnAll {
applyCSVColumnPlacements(recs)
outRows = recs
} else if len(recs) > 0 {
// map the header names to their field index
hdrs := make(map[string]int, len(recs[0]))
for i, hdr := range recs[0] {
hdrs[hdr] = i
}
if len(recs) > 0 {
// map the header names to their field index
hdrs := make(map[string]int, len(recs))
for i, hdr := range recs[0] {
hdrs[hdr] = i
}
outRows = make([][]string, len(recs))
for i, rec := range recs {
for _, col := range r.Columns {
colIx, ok := hdrs[col]
if !ok {
// invalid column name - it would be nice to catch this in the
// endpoint before processing the results, but it would require
// duplicating the list of columns from the Host's struct tags to a
// map and keep this in sync, for what is essentially a programmer
// mistake that should be caught and corrected early.
encodeError(ctx, &fleet.BadRequestError{Message: fmt.Sprintf("invalid column name: %q", col)}, w)
return
}
outRows[i] = append(outRows[i], rec[colIx])
outRows = make([][]string, len(recs))
for i, rec := range recs {
for _, col := range r.Columns {
colIx, ok := hdrs[col]
if !ok {
// invalid column name - it would be nice to catch this in the
// endpoint before processing the results, but it would require
// duplicating the list of columns from the Host's struct tags to a
// map and keep this in sync, for what is essentially a programmer
// mistake that should be caught and corrected early.
encodeError(ctx, &fleet.BadRequestError{Message: fmt.Sprintf("invalid column name: %q", col)}, w)
return
}
outRows[i] = append(outRows[i], rec[colIx])
}
}
}
@@ -3132,17 +3133,69 @@ func (r hostsReportResponse) HijackRender(ctx context.Context, w http.ResponseWr
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
var err error
if returnAll {
_, err = io.Copy(w, &buf)
} else {
err = csv.NewWriter(w).WriteAll(outRows)
}
if err != nil {
if err := csv.NewWriter(w).WriteAll(outRows); err != nil {
logging.WithErr(ctx, err)
}
}
// csvColumnPlacements forces the ordering of columns in the full (unfiltered)
// hosts report CSV. gocsv appends HostResponse fields after all Host columns,
// so columns whose documented position is elsewhere must be moved explicitly.
// The filtered path already emits columns in the requested order.
var csvColumnPlacements = []struct{ col, after string }{
{"hardware_marketing_name", "hardware_model"},
}
func applyCSVColumnPlacements(recs [][]string) {
for _, p := range csvColumnPlacements {
reorderCSVColumnAfter(recs, p.col, p.after)
}
}
// reorderCSVColumnAfter moves the column named col so that it immediately
// follows the column named afterCol in every record (header + rows). It is a
// no-op if either column is missing.
func reorderCSVColumnAfter(recs [][]string, col, afterCol string) {
if len(recs) == 0 {
return
}
from, after := -1, -1
for i, hdr := range recs[0] {
switch hdr {
case col:
from = i
case afterCol:
after = i
}
}
if from < 0 || after < 0 || from == after {
return
}
// Build the new column index order with `from` placed right after `after`.
order := make([]int, 0, len(recs[0]))
for i := range recs[0] {
if i == from {
continue
}
order = append(order, i)
if i == after {
order = append(order, from)
}
}
for r, rec := range recs {
newRec := make([]string, len(order))
for j, idx := range order {
if idx < len(rec) {
newRec[j] = rec[idx]
}
}
recs[r] = newRec
}
}
func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*hostsReportRequest)
+131
View File
@@ -5705,3 +5705,134 @@ func TestGetHostDEPAssignmentDetailsNotFoundClassification(t *testing.T) {
})
}
}
func TestReorderCSVColumnAfter(t *testing.T) {
cases := []struct {
name string
recs [][]string
col string
afterCol string
want [][]string
}{
{
name: "moves a column that comes after its target",
recs: [][]string{
{"a", "model", "b", "c", "marketing"},
{"1", "m1", "2", "3", "mk1"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"a", "model", "marketing", "b", "c"},
{"1", "m1", "mk1", "2", "3"},
},
},
{
name: "reorders every data row",
recs: [][]string{
{"a", "model", "b", "marketing"},
{"1", "m1", "2", "mk1"},
{"3", "m2", "4", "mk2"},
{"5", "m3", "6", "mk3"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"a", "model", "marketing", "b"},
{"1", "m1", "mk1", "2"},
{"3", "m2", "mk2", "4"},
{"5", "m3", "mk3", "6"},
},
},
{
name: "moves a column that comes before its target",
recs: [][]string{
{"marketing", "a", "model", "b"},
{"mk1", "1", "m1", "2"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"a", "model", "marketing", "b"},
{"1", "m1", "mk1", "2"},
},
},
{
name: "already immediately after target is unchanged",
recs: [][]string{
{"model", "marketing", "b"},
{"m1", "mk1", "2"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"model", "marketing", "b"},
{"m1", "mk1", "2"},
},
},
{
name: "header-only records are reordered",
recs: [][]string{
{"a", "marketing", "model", "b"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"a", "model", "marketing", "b"},
},
},
{
name: "missing col is a no-op",
recs: [][]string{
{"model", "b"},
{"m1", "2"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"model", "b"},
{"m1", "2"},
},
},
{
name: "missing afterCol is a no-op",
recs: [][]string{
{"marketing", "b"},
{"mk1", "2"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"marketing", "b"},
{"mk1", "2"},
},
},
{
name: "empty records is a no-op",
recs: [][]string{},
col: "marketing",
afterCol: "model",
want: [][]string{},
},
{
name: "ragged rows are padded without panicking",
recs: [][]string{
{"a", "model", "b", "marketing"},
{"1", "m1"},
},
col: "marketing",
afterCol: "model",
want: [][]string{
{"a", "model", "marketing", "b"},
{"1", "m1", "", ""},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
reorderCSVColumnAfter(c.recs, c.col, c.afterCol)
require.Equal(t, c.want, c.recs)
})
}
}
+79 -7
View File
@@ -10832,19 +10832,21 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows, len(hosts)+1) // all hosts + header row
assert.Len(t, rows[0], 57) // total number of cols
assert.Len(t, rows[0], 58) // total number of cols
// Validate that both team_id and fleet_id columns are present.
assert.Contains(t, rows[0], "team_id")
assert.Contains(t, rows[0], "fleet_id")
assert.Contains(t, rows[0], "team_name")
assert.Contains(t, rows[0], "fleet_name")
// hardware_marketing_name is emitted right after hardware_model, shifting
// every subsequent column index by one.
const (
idCol = 3
issuesCol = 46
gigsDiskCol = 42
pctDiskCol = 43
gigsTotalCol = 44
issuesCol = 47
gigsDiskCol = 43
pctDiskCol = 44
gigsTotalCol = 45
)
// find the row for hosts[1], it should have issues=1 (1 failing policy) and the expected disk space
@@ -10880,6 +10882,21 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
require.Contains(t, res.Header.Get("Content-Type"), "text/csv")
require.Contains(t, res.Header.Get("X-Content-Type-Options"), "nosniff")
// requesting columns that don't include hardware_model or
// hardware_marketing_name returns exactly the requested columns and neither
// hardware field (hardware_marketing_name must not leak into the report).
res = s.DoRaw(
"GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv",
"columns", "hostname,uuid,platform",
)
rows, err = csv.NewReader(res.Body).ReadAll()
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows, len(hosts)+1)
require.Equal(t, []string{"hostname", "uuid", "platform"}, rows[0])
require.NotContains(t, rows[0], "hardware_model")
require.NotContains(t, rows[0], "hardware_marketing_name")
// pagination does not apply to this endpoint, it returns the complete list of hosts
res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "page", "1", "per_page", "2", "columns", "hostname")
rows, err = csv.NewReader(res.Body).ReadAll()
@@ -10982,6 +10999,61 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusBadRequest, "software_id", "123", "software_version_id", "456", "software_title_id", "789")
}
func (s *integrationTestSuite) TestHostsReportHardwareMarketingName() {
t := s.T()
ctx := context.Background()
newHost := func(suffix, platform, model string) *fleet.Host {
h, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: new(t.Name() + suffix),
NodeKey: new(t.Name() + suffix),
UUID: uuid.New().String(),
Hostname: t.Name() + suffix,
Platform: platform,
})
require.NoError(t, err)
// hardware_model is not persisted by NewHost, so set it directly.
mysqltest.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx, `UPDATE hosts SET hardware_model = ? WHERE id = ?`, model, h.ID)
return err
})
return h
}
// Apple host whose model maps to a marketing name, plus a non-Apple host
// with no mapping.
mapped := newHost("-mapped", "darwin", "MacBookPro18,1")
unmapped := newHost("-unmapped", "ubuntu", "Standard PC")
res := s.DoRaw(
"GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv",
"columns", "hostname,hardware_model,hardware_marketing_name",
)
rows, err := csv.NewReader(res.Body).ReadAll()
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows, 3) // header + 2 hosts
// columns are returned in the requested order
require.Equal(t, []string{"hostname", "hardware_model", "hardware_marketing_name"}, rows[0])
byHostname := make(map[string][]string, len(rows)-1)
for _, row := range rows[1:] {
byHostname[row[0]] = row
}
// Apple host: raw model plus the mapped marketing name.
require.Equal(t, "MacBookPro18,1", byHostname[mapped.Hostname][1])
require.Equal(t, fleet.AppleHardwareModelsToMarketingNames["MacBookPro18,1"], byHostname[mapped.Hostname][2])
// Non-Apple host: raw model, empty marketing name.
require.Equal(t, "Standard PC", byHostname[unmapped.Hostname][1])
require.Empty(t, byHostname[unmapped.Hostname][2])
}
func (s *integrationTestSuite) TestSSODisabled() {
t := s.T()
@@ -13229,7 +13301,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows1, len(hosts)+1) // all hosts + header row
assert.Len(t, rows1[0], 57) // total number of cols
assert.Len(t, rows1[0], 58) // total number of cols
var (
idIdx int
@@ -13259,7 +13331,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows2, len(hosts)+1) // all hosts + header row
assert.Len(t, rows2[0], 57) // total number of cols
assert.Len(t, rows2[0], 58) // total number of cols
// Check that all hosts have 0 issues and that they match the previous call to `/hosts/report`.
for i := 1; i < len(hosts)+1; i++ {
+1
View File
@@ -25364,6 +25364,7 @@ func (s *integrationMDMTestSuite) TestErrorOnEnrollmentInstallProfileProducesAct
require.NoError(t, apple_mdm.HandleHostMDMProfileInstallResult(ctx, s.ds, host.UUID, case4RenewCmd, &verifying, "", s.fleetSvc.NewActivity))
require.Zero(t, countRenewalActivitiesForCmd(case4RenewCmd))
}
func (s *integrationMDMTestSuite) TestInstallAllSelfServiceSoftware() {
t := s.T()
ctx := context.Background()