Add ability to see Google Chrome profiles on the Hosts page (#5839)

This commit is contained in:
gillespi314 2022-05-23 14:27:30 -05:00 committed by GitHub
parent 2db2c16511
commit bbc1891420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 216 additions and 90 deletions

View file

@ -0,0 +1,3 @@
- Added google chrome profile information to hosts page
- Increased MySQL `group_concat_max_len` setting from default 1024 to 4194304 (which corresponds to
`max_allowed_packet` size)

View file

@ -71,6 +71,22 @@ describe("Hosts flow", () => {
});
}
});
it(`hides and shows "Used by" column`, () => {
cy.visit("/hosts/manage");
cy.getAttached("thead").within(() =>
cy.findByText(/used by/i).should("not.exist")
);
cy.getAttached(".table-container").within(() => {
cy.contains("button", /edit columns/i).click();
});
cy.getAttached(".edit-columns-modal").within(() => {
cy.findByLabelText(/used by/i).check({ force: true });
cy.contains("button", /save/i).click();
});
cy.getAttached("thead").within(() =>
cy.findByText(/used by/i).should("exist")
);
});
});
describe("Manage policies page", () => {
beforeEach(() => {

View file

@ -2,8 +2,10 @@
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { Column } from "react-table";
import ReactTooltip from "react-tooltip";
import { IHost } from "interfaces/host";
import { IDeviceUser, IHost } from "interfaces/host";
import Checkbox from "components/forms/fields/Checkbox";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import IssueCell from "components/TableContainer/DataTable/IssueCell/IssueCell";
@ -18,6 +20,7 @@ import {
hostTeamName,
} from "utilities/helpers";
import { IConfig } from "interfaces/config";
import { IDataColumn } from "interfaces/datatable_config";
import { ITeamSummary } from "interfaces/team";
import { IUser } from "interfaces/user";
import PATHS from "router/paths";
@ -51,16 +54,40 @@ interface ICellProps {
};
}
interface IHostDataColumn {
Header: ((props: IHeaderProps) => JSX.Element) | string;
Cell: (props: ICellProps) => JSX.Element;
id?: string;
title?: string;
accessor?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
interface IDeviceUserCellProps {
cell: {
value: IDeviceUser[];
};
row: {
original: IHost;
};
}
const condenseDeviceUsers = (users: IDeviceUser[]): string[] => {
if (!users?.length) {
return [];
}
const condensed =
users
.slice(-3)
.map((u) => u.email)
.reverse() || [];
return users.length > 3
? condensed.concat(`+${users.length - 3} more`) // TODO: confirm limit
: condensed;
};
const tooltipTextWithLineBreaks = (lines: string[]) => {
return lines.map((line) => {
return (
<span key={Math.random().toString().slice(2)}>
{line}
<br />
</span>
);
});
};
const lastSeenTime = (status: string, seenTime: string): string => {
if (status !== "online") {
return `Last Seen: ${humanHostLastSeen(seenTime)} UTC`;
@ -68,7 +95,7 @@ const lastSeenTime = (status: string, seenTime: string): string => {
return "Online";
};
const allHostTableHeaders: IHostDataColumn[] = [
const allHostTableHeaders: IDataColumn[] = [
// We are using React Table useRowSelect functionality for the selection header.
// More information on its API can be found here
// https://react-table.tanstack.com/docs/api/useRowSelect
@ -95,14 +122,14 @@ const allHostTableHeaders: IHostDataColumn[] = [
},
{
title: "Hostname",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "hostname",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<LinkCell
value={cellProps.cell.value}
path={PATHS.HOST_DETAILS(cellProps.row.original)}
@ -116,14 +143,14 @@ const allHostTableHeaders: IHostDataColumn[] = [
},
{
title: "Team",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "team_name",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<TextCell value={cellProps.cell.value} formatter={hostTeamName} />
),
},
@ -132,14 +159,16 @@ const allHostTableHeaders: IHostDataColumn[] = [
Header: "Status",
disableSortBy: true,
accessor: "status",
Cell: (cellProps) => <StatusCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => (
<StatusCell value={cellProps.cell.value} />
),
},
{
title: "Issues",
Header: () => <img alt="host issues" src={IssueIcon} />,
disableSortBy: true,
accessor: "issues",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<IssueCell
issues={cellProps.row.original.issues}
rowId={cellProps.row.original.id}
@ -148,47 +177,83 @@ const allHostTableHeaders: IHostDataColumn[] = [
},
{
title: "OS",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "os_version",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Osquery",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "osquery_version",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Used by",
Header: "Used by",
disableSortBy: true,
accessor: "device_mapping",
Cell: (cellProps: IDeviceUserCellProps): JSX.Element => {
const numUsers = cellProps.cell.value?.length || 0;
const users = condenseDeviceUsers(cellProps.cell.value || []);
if (users.length) {
const tooltipText = tooltipTextWithLineBreaks(users);
return (
<>
<span
className={`text-cell ${users.length > 1 ? "text-muted" : ""}`}
data-tip
data-for={`device_mapping__${cellProps.row.original.id}`}
data-tip-disable={users.length <= 1}
>
{numUsers === 1 ? users[0] : `${numUsers} users`}
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`device_mapping__${cellProps.row.original.id}`}
data-html
>
<span className={`tooltip__tooltip-text`}>{tooltipText}</span>
</ReactTooltip>
</>
);
}
return <span className="text-muted">---</span>;
},
},
{
title: "IP address",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "primary_ip",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Last fetched",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "detail_updated_at",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<TextCell
value={cellProps.cell.value}
formatter={humanHostDetailUpdated}
@ -197,38 +262,38 @@ const allHostTableHeaders: IHostDataColumn[] = [
},
{
title: "Last seen",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "seen_time",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<TextCell value={cellProps.cell.value} formatter={humanHostLastSeen} />
),
},
{
title: "UUID",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "uuid",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Uptime",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "uptime",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<TextCell value={cellProps.cell.value} formatter={humanHostUptime} />
),
},
@ -237,57 +302,58 @@ const allHostTableHeaders: IHostDataColumn[] = [
Header: "CPU",
disableSortBy: true,
accessor: "cpu_type",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "RAM",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "memory",
Cell: (cellProps) => (
Cell: (cellProps: ICellProps) => (
<TextCell value={cellProps.cell.value} formatter={humanHostMemory} />
),
},
{
title: "MAC address",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "primary_mac",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Serial number",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "hardware_serial",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Hardware model",
Header: (cellProps) => (
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "hardware_model",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
];
const defaultHiddenColumns = [
"device_mapping",
"primary_mac",
"cpu_type",
"memory",
@ -306,9 +372,9 @@ const generateAvailableTableHeaders = (
config: IConfig,
currentUser: IUser,
currentTeam: ITeamSummary | undefined
): IHostDataColumn[] => {
): IDataColumn[] => {
return allHostTableHeaders.reduce(
(columns: IHostDataColumn[], currentColumn: IHostDataColumn) => {
(columns: Column[], currentColumn: Column) => {
// skip over column headers that are not shown in free observer tier
if (
permissionUtils.isFreeTier(config) &&
@ -356,7 +422,7 @@ const generateVisibleTableColumns = (
config: IConfig,
currentUser: IUser,
currentTeam: ITeamSummary | undefined
): IHostDataColumn[] => {
): IDataColumn[] => {
// remove columns set as hidden by the user.
return generateAvailableTableHeaders(config, currentUser, currentTeam).filter(
(column) => {

View file

@ -478,7 +478,7 @@ const ManageHostsPage = ({
if (options.sortBy) {
delete options.sortBy;
}
retrieveHostCount(options);
retrieveHostCount(omit(options, "device_mapping"));
};
let teamSync = false;
@ -519,6 +519,7 @@ const ManageHostsPage = ({
softwareId,
page: tableQueryData ? tableQueryData.pageIndex : 0,
perPage: tableQueryData ? tableQueryData.pageSize : 100,
device_mapping: true,
};
if (isEqual(options, currentQueryOptions)) {
@ -526,7 +527,7 @@ const ManageHostsPage = ({
}
if (teamSync) {
retrieveHosts(options);
retrieveHostCount(options);
retrieveHostCount(omit(options, "device_mapping"));
setCurrentQueryOptions(options);
}
}, [availableTeams, currentTeam, location, labels]);

View file

@ -127,6 +127,14 @@
}
}
}
.device_mapping__cell {
.text-cell {
display: inline;
}
.text-muted {
color: $ui-fleet-black-50;
}
}
}
}
}

View file

@ -389,8 +389,8 @@
text-align: center;
}
&__device-mapping {
.device-user-tooltip {
&__device_mapping {
.device_mapping--tooltip {
flex-direction: column;
justify-content: start;
text-align: left;

View file

@ -404,8 +404,8 @@
margin-right: $pad-small;
}
&__device-mapping {
.device-user-tooltip {
&__device_mapping {
.device_mapping--tooltip {
flex-direction: column;
justify-content: start;
text-align: left;

View file

@ -81,48 +81,44 @@ const About = ({
};
const renderDeviceUser = () => {
const numUsers = deviceMapping?.length;
if (numUsers) {
return (
<div className="info-grid__block">
<span className="info-grid__header">Used by</span>
<span className="info-grid__data">
{numUsers === 1 && deviceMapping ? (
deviceMapping[0].email || "---"
) : (
<span className={`${baseClass}__device-mapping`}>
<span
className="device-user"
data-tip
data-for="device-user-tooltip"
>
{`${numUsers} users`}
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
id="device-user-tooltip"
backgroundColor="#3e4771"
>
<div
className={`${baseClass}__tooltip-text device-user-tooltip`}
>
{deviceMapping &&
deviceMapping.map((user: any, i: number, arr: any) => (
<span key={user.email}>{`${user.email}${
i < arr.length - 1 ? ", " : ""
}`}</span>
))}
</div>
</ReactTooltip>
</span>
)}
</span>
</div>
);
if (!deviceMapping) {
return null;
}
return null;
const numUsers = deviceMapping.length;
const tooltipText = deviceMapping.map((d) => (
<span key={Math.random().toString().slice(2)}>
{d.email}
<br />
</span>
));
return (
<div className="info-grid__block">
<span className="info-grid__header">Used by</span>
<span className="info-grid__data">
{numUsers > 1 ? (
<>
<span data-tip data-for={`device_mapping`}>
{`${numUsers} users`}
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`device_mapping`}
data-html
>
<span className={`tooltip__tooltip-text`}>{tooltipText}</span>
</ReactTooltip>
</>
) : (
deviceMapping[0].email || "---"
)}
</span>
</div>
);
};
const renderGeolocation = () => {

View file

@ -18,6 +18,7 @@ export interface ILoadHostsOptions {
policyId?: number;
policyResponse?: string;
softwareId?: number;
device_mapping?: boolean;
columns?: string;
}
@ -127,6 +128,7 @@ export default {
const policyId = options?.policyId || null;
const policyResponse = options?.policyResponse || null;
const softwareId = options?.softwareId || null;
const device_mapping = options?.device_mapping || null;
const columns = options?.columns || null;
// TODO: add this query param logic to client class
@ -183,6 +185,10 @@ export default {
path += `&software_id=${softwareId}`;
}
if (device_mapping) {
path += "&device_mapping=true";
}
if (columns) {
path += `&columns=${columns}`;
}

View file

@ -834,7 +834,7 @@ func registerTLS(conf config.MysqlConfig) error {
func generateMysqlConnectionString(conf config.MysqlConfig) string {
tz := url.QueryEscape("'-00:00'")
dsn := fmt.Sprintf(
"%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC&time_zone=%s&clientFoundRows=true&allowNativePasswords=true",
"%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC&time_zone=%s&clientFoundRows=true&allowNativePasswords=true&group_concat_max_len=4194304",
conf.Username,
conf.Password,
conf.Protocol,

View file

@ -19,6 +19,7 @@ import (
"testing"
"time"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
@ -2189,6 +2190,35 @@ func (s *integrationTestSuite) TestHostDeviceMapping() {
require.Len(t, listHosts.Hosts, 0)
}
func (s *integrationTestSuite) TestListHostsDeviceMappingSize() {
t := s.T()
ctx := context.Background()
hosts := s.createHosts(t)
testSize := 50
var mappings []*fleet.HostDeviceMapping
for i := 0; i < testSize; i++ {
testEmail, _ := server.GenerateRandomText(14)
mappings = append(mappings, &fleet.HostDeviceMapping{HostID: hosts[0].ID, Email: testEmail, Source: "google_chrome_profiles"})
}
s.ds.ReplaceHostDeviceMapping(ctx, hosts[0].ID, mappings)
var listHosts listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts?device_mapping=true", nil, http.StatusOK, &listHosts)
hostsByID := make(map[uint]HostResponse)
for _, h := range listHosts.Hosts {
hostsByID[h.ID] = h
}
require.NotNil(t, *hostsByID[hosts[0].ID].DeviceMapping)
var dm []*fleet.HostDeviceMapping
err := json.Unmarshal(*hostsByID[hosts[0].ID].DeviceMapping, &dm)
require.NoError(t, err)
require.Len(t, dm, testSize)
}
func (s *integrationTestSuite) TestGetMacadminsData() {
t := s.T()