ChromeOS tables: Errors surfaced in Fleet UI (#12376)

This commit is contained in:
RachelElysia 2023-09-19 10:06:29 -04:00 committed by GitHub
parent 414c2f42b3
commit 9c5d7faa58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 308 additions and 196 deletions

View File

@ -0,0 +1 @@
- UI improvement: Surface chrome live query errors to Fleet UI (including errors for specific columns while maintaining successful data in results)

View File

@ -12,6 +12,7 @@ interface requestArgs {
body?: Record<string, any>; body?: Record<string, any>;
reenroll?: boolean; reenroll?: boolean;
} }
const request = async ({ path, body = {} }: requestArgs): Promise<any> => { const request = async ({ path, body = {} }: requestArgs): Promise<any> => {
const { fleet_url } = await chrome.storage.managed.get({ const { fleet_url } = await chrome.storage.managed.get({
fleet_url: FLEET_URL, fleet_url: FLEET_URL,
@ -67,8 +68,8 @@ const authenticatedRequest = async ({
}; };
const enroll = async () => { const enroll = async () => {
const os_version = await DATABASE.query("SELECT * FROM os_version"); const os_version = (await DATABASE.query("SELECT * FROM os_version")).data;
const system_info = await DATABASE.query("SELECT * FROM system_info"); const system_info = (await DATABASE.query("SELECT * FROM system_info")).data;
const host_details = { const host_details = {
os_version: os_version[0], os_version: os_version[0],
system_info: system_info[0], system_info: system_info[0],
@ -118,7 +119,8 @@ const live_query = async () => {
const query_discovery_sql = response.discovery[query_name]; const query_discovery_sql = response.discovery[query_name];
if (query_discovery_sql) { if (query_discovery_sql) {
try { try {
const discovery_result = await DATABASE.query(query_discovery_sql); const discovery_result = (await DATABASE.query(query_discovery_sql))
.data;
if (discovery_result.length == 0) { if (discovery_result.length == 0) {
// Discovery queries that return no results mean skip running the query. // Discovery queries that return no results mean skip running the query.
continue; continue;
@ -129,6 +131,9 @@ const live_query = async () => {
console.debug( console.debug(
`Discovery (${query_name} sql: "${query_discovery_sql}") failed: ${err}` `Discovery (${query_name} sql: "${query_discovery_sql}") failed: ${err}`
); );
results[query_name] = null;
statuses[query_name] = 1;
messages[query_name] = err.toString();
continue; continue;
} }
} }
@ -137,8 +142,12 @@ const live_query = async () => {
const query_sql = response.queries[query_name]; const query_sql = response.queries[query_name];
try { try {
const query_result = await DATABASE.query(query_sql); const query_result = await DATABASE.query(query_sql);
results[query_name] = query_result; results[query_name] = query_result.data;
statuses[query_name] = 0; statuses[query_name] = 0;
if (query_result.warnings.length !== 0) {
statuses[query_name] = 1; // Set to show warnings in errors table and campaign.ts returned host_counts to +1 failing instead of +1 successful
messages[query_name] = query_result.warnings; // Warnings array is concatenated in Table.ts xfilter
}
} catch (err) { } catch (err) {
console.warn(`Query (${query_name} sql: "${query_sql}") failed: ${err}`); console.warn(`Query (${query_name} sql: "${query_sql}") failed: ${err}`);
results[query_name] = null; results[query_name] = null;

View File

@ -15,9 +15,20 @@ import TableSystemInfo from "./tables/system_info";
import TableSystemState from "./tables/system_state"; import TableSystemState from "./tables/system_state";
import TableUsers from "./tables/users"; import TableUsers from "./tables/users";
interface ChromeWarning {
column: string;
error_message: string;
}
interface ChromeResponse {
data: Record<string, string>[];
/** Manually add errors in catch response if table requires multiple APIs requests */
warnings?: ChromeWarning[];
}
export default class VirtualDatabase { export default class VirtualDatabase {
sqlite3: SQLiteAPI; sqlite3: SQLiteAPI;
db: number; db: number;
warnings?: ChromeWarning[];
private constructor(sqlite3: SQLiteAPI, db: number) { private constructor(sqlite3: SQLiteAPI, db: number) {
this.sqlite3 = sqlite3; this.sqlite3 = sqlite3;
@ -42,7 +53,11 @@ export default class VirtualDatabase {
new TablePrivacyPreferences(sqlite3, db) new TablePrivacyPreferences(sqlite3, db)
); );
VirtualDatabase.register(sqlite3, db, new TableScreenLock(sqlite3, db)); VirtualDatabase.register(sqlite3, db, new TableScreenLock(sqlite3, db));
VirtualDatabase.register(sqlite3, db, new TableSystemInfo(sqlite3, db)); VirtualDatabase.register(
sqlite3,
db,
new TableSystemInfo(sqlite3, db, this.warnings)
);
VirtualDatabase.register(sqlite3, db, new TableSystemState(sqlite3, db)); VirtualDatabase.register(sqlite3, db, new TableSystemState(sqlite3, db));
VirtualDatabase.register(sqlite3, db, new TableOSVersion(sqlite3, db)); VirtualDatabase.register(sqlite3, db, new TableOSVersion(sqlite3, db));
VirtualDatabase.register(sqlite3, db, new TableOsqueryInfo(sqlite3, db)); VirtualDatabase.register(sqlite3, db, new TableOsqueryInfo(sqlite3, db));
@ -60,7 +75,7 @@ export default class VirtualDatabase {
sqlite3.create_module(db, table.name, table); sqlite3.create_module(db, table.name, table);
} }
async query(sql: string): Promise<Record<string, string | number>[]> { async query(sql: string): Promise<ChromeResponse> {
let rows = []; let rows = [];
await this.sqlite3.exec(this.db, sql, (row, columns) => { await this.sqlite3.exec(this.db, sql, (row, columns) => {
// map each row to object // map each row to object
@ -68,6 +83,6 @@ export default class VirtualDatabase {
Object.fromEntries(columns.map((_, i) => [columns[i], row[i]])) Object.fromEntries(columns.map((_, i) => [columns[i], row[i]]))
); );
}); });
return rows; return { data: rows, warnings: this.warnings };
} }
} }

View File

@ -4,29 +4,48 @@
import * as SQLite from "wa-sqlite"; import * as SQLite from "wa-sqlite";
/** Creates a single UI friendly string out of chrome tables that return multiple warnings */
const CONCAT_CHROME_WARNINGS = (warnings: ChromeWarning[]): string => {
const warningStrings = warnings.map(
(warning) => `Column: ${warning.column} - ${warning.error_message}`
);
return warningStrings.join("\n");
};
class cursorState { class cursorState {
rowIndex: number; rowIndex: number;
rows: Record<string, string>[]; rows: Record<string, string>[];
error: any; error: any;
} }
interface ChromeWarning {
column: string;
error_message: string;
}
interface ChromeResponse {
data: Record<string, string>[];
/** Manually add errors in catch response if table requires requests to multiple APIs */
warnings?: ChromeWarning[];
}
export default abstract class Table implements SQLiteModule { export default abstract class Table implements SQLiteModule {
sqlite3: SQLiteAPI; sqlite3: SQLiteAPI;
db: number; db: number;
name: string; name: string;
columns: string[]; columns: string[];
cursorStates: Map<number, cursorState>; cursorStates: Map<number, cursorState>;
warnings?: ChromeWarning[];
abstract generate( abstract generate(
idxNum: number, idxNum: number,
idxString: string, idxString: string,
values: Array<number> values: Array<number>
): Promise<Record<string, string>[]>; ): Promise<ChromeResponse>;
constructor(sqlite3: SQLiteAPI, db: number) { constructor(sqlite3: SQLiteAPI, db: number, warnings?: ChromeWarning[]) {
this.sqlite3 = sqlite3; this.sqlite3 = sqlite3;
this.db = db; this.db = db;
this.cursorStates = new Map(); this.cursorStates = new Map();
this.warnings = warnings;
} }
// This is replaced by wa-sqlite when SQLite is loaded up, but missing from the SQLiteModule // This is replaced by wa-sqlite when SQLite is loaded up, but missing from the SQLiteModule
@ -91,7 +110,16 @@ export default abstract class Table implements SQLiteModule {
const cursorState = this.cursorStates.get(pCursor); const cursorState = this.cursorStates.get(pCursor);
cursorState.rowIndex = 0; cursorState.rowIndex = 0;
try { try {
cursorState.rows = await this.generate(idxNum, idxStr, values); const tableDataReturned = await this.generate(idxNum, idxStr, values);
// Set warnings to this.warnings for database to surface in UI
if (tableDataReturned.warnings) {
globalThis.DB.warnings = []; // Reset warnings
globalThis.DB.warnings = CONCAT_CHROME_WARNINGS(
tableDataReturned.warnings
);
}
cursorState.rows = tableDataReturned.data;
} catch (err) { } catch (err) {
// Throwing here doesn't seem to work as expected in testing (the error doesn't seem to be // Throwing here doesn't seem to work as expected in testing (the error doesn't seem to be
// thrown in a way that it can be caught appropriately), so instead we save the error and // thrown in a way that it can be caught appropriately), so instead we save the error and

View File

@ -36,6 +36,6 @@ export default class TableChromeExtensions extends Table {
}); });
} }
return rows; return { data: rows };
} }
} }

View File

@ -1,6 +1,7 @@
import VirtualDatabase from "../db"; import VirtualDatabase from "../db";
const DISK_INFO_MOCK = [ const DISK_INFO_MOCK = {
data: [
{ {
capacity: 1234, capacity: 1234,
id: 123, id: 123,
@ -13,7 +14,8 @@ const DISK_INFO_MOCK = [
name: "Thumbdrive", name: "Thumbdrive",
type: "Removable", type: "Removable",
}, },
]; ],
};
describe("disk_info", () => { describe("disk_info", () => {
test("success", async () => { test("success", async () => {

View File

@ -15,6 +15,6 @@ export default class TableDiskInfo extends Table {
type: d.type, type: d.type,
}); });
} }
return rows; return { data: rows };
} }
} }

View File

@ -45,14 +45,16 @@ describe("geolocation", () => {
}) })
); );
const rows = await db.query("select * from geolocation"); const rows = await db.query("select * from geolocation");
expect(rows).toEqual([ expect(rows).toEqual({
data: [
{ {
ip: "260f:1337:4a7e:e300:abcd:a98a:1234:18c", ip: "260f:1337:4a7e:e300:abcd:a98a:1234:18c",
city: "Vancouver", city: "Vancouver",
country: "Canada", country: "Canada",
region: "British Columbia", region: "British Columbia",
}, },
]); ],
});
}); });
test("request returns incomplete data", async () => { test("request returns incomplete data", async () => {
@ -66,14 +68,16 @@ describe("geolocation", () => {
}) })
); );
const rows = await db.query("select * from geolocation"); const rows = await db.query("select * from geolocation");
expect(rows).toEqual([ expect(rows).toEqual({
data: [
{ {
ip: null, ip: null,
city: "Vancouver", city: "Vancouver",
country: null, country: null,
region: null, region: null,
}, },
]); ],
});
}); });
test("request fails", async () => { test("request fails", async () => {

View File

@ -14,13 +14,15 @@ export default class TableGeolocation extends Table {
async generate() { async generate() {
const resp = await fetch("https://ipapi.co/json"); const resp = await fetch("https://ipapi.co/json");
const json = await resp.json(); const json = await resp.json();
return [ return {
data: [
{ {
ip: this.ensureString(json.ip), ip: this.ensureString(json.ip),
city: this.ensureString(json.city), city: this.ensureString(json.city),
country: this.ensureString(json.country_name), country: this.ensureString(json.country_name),
region: this.ensureString(json.region), region: this.ensureString(json.region),
}, },
]; ],
};
} }
} }

View File

@ -5,22 +5,20 @@ export default class TableNetworkInterfaces extends Table {
columns = ["mac", "ipv4", "ipv6"]; columns = ["mac", "ipv4", "ipv6"];
async generate() { async generate() {
let ipv4: string, ipv6: string, mac: string;
try {
// @ts-expect-error @types/chrome doesn't yet have the getNetworkDetails Promise API. // @ts-expect-error @types/chrome doesn't yet have the getNetworkDetails Promise API.
const networkDetails = (await chrome.enterprise.networkingAttributes.getNetworkDetails()) as chrome.enterprise.networkingAttributes.NetworkDetails; const networkDetails = (await chrome.enterprise.networkingAttributes.getNetworkDetails()) as chrome.enterprise.networkingAttributes.NetworkDetails;
ipv4 = networkDetails.ipv4; const ipv4 = networkDetails.ipv4;
ipv6 = networkDetails.ipv6; const ipv6 = networkDetails.ipv6;
mac = networkDetails.macAddress; const mac = networkDetails.macAddress;
} catch (err) {
console.warn(`get network details: ${err}`); return {
} data: [
return [
{ {
mac, mac,
ipv4, ipv4,
ipv6, ipv6,
}, },
]; ],
};
} }
} }

View File

@ -3,18 +3,18 @@ import TableOSVersion from "./os_version";
describe("os_version", () => { describe("os_version", () => {
describe("getName", () => { describe("getName", () => {
const sut = new TableOSVersion(null, null) const sut = new TableOSVersion(null, null);
it("returns platform name properly formatted", () => { it("returns platform name properly formatted", () => {
expect(sut.getName("Chrome OS")).toBe("ChromeOS") expect(sut.getName("Chrome OS")).toBe("ChromeOS");
}) });
}) });
describe("getCodename", () => { describe("getCodename", () => {
const sut = new TableOSVersion(null, null) const sut = new TableOSVersion(null, null);
it("has the proper prefix", () => { it("has the proper prefix", () => {
expect(sut.getCodename("10.0.0").startsWith("ChromeOS")).toBe(true) expect(sut.getCodename("10.0.0").startsWith("ChromeOS")).toBe(true);
}) });
}) });
test("success", async () => { test("success", async () => {
// @ts-expect-error Typescript doesn't include the userAgentData API yet. // @ts-expect-error Typescript doesn't include the userAgentData API yet.
@ -40,7 +40,8 @@ describe("os_version", () => {
const db = await VirtualDatabase.init(); const db = await VirtualDatabase.init();
const res = await db.query("select * from os_version"); const res = await db.query("select * from os_version");
expect(res).toEqual([ expect(res).toEqual({
data: [
{ {
name: "ChromeOS", name: "ChromeOS",
platform: "chrome", platform: "chrome",
@ -53,7 +54,8 @@ describe("os_version", () => {
arch: "x86-64", arch: "x86-64",
codename: "ChromeOS 13.2.1", codename: "ChromeOS 13.2.1",
}, },
]); ],
});
}); });
test("unexpected version string", async () => { test("unexpected version string", async () => {
@ -81,7 +83,8 @@ describe("os_version", () => {
const db = await VirtualDatabase.init(); const db = await VirtualDatabase.init();
const res = await db.query("select * from os_version"); const res = await db.query("select * from os_version");
expect(res).toEqual([ expect(res).toEqual({
data: [
{ {
name: "ChromeOS", name: "ChromeOS",
platform: "chrome", platform: "chrome",
@ -94,7 +97,8 @@ describe("os_version", () => {
arch: "x86-64", arch: "x86-64",
codename: "ChromeOS 13.2.1", codename: "ChromeOS 13.2.1",
}, },
]); ],
});
expect(console.warn).toHaveBeenCalledWith( expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining("expected 4 segments") expect.stringContaining("expected 4 segments")
); );
@ -105,9 +109,11 @@ describe("os_version", () => {
global.navigator.userAgentData = { global.navigator.userAgentData = {
getHighEntropyValues: jest.fn(() => getHighEntropyValues: jest.fn(() =>
Promise.resolve({ Promise.resolve({
data: {
fullVersionList: [ fullVersionList: [
{ brand: "Not even chrome", version: "110.0.5481.177" }, { brand: "Not even chrome", version: "110.0.5481.177" },
], ],
},
}) })
), ),
}; };

View File

@ -63,7 +63,8 @@ export default class TableOSVersion extends Table {
const { arch } = platformInfo; const { arch } = platformInfo;
// Some of these values won't actually be correct on a non-chromeOS machine. // Some of these values won't actually be correct on a non-chromeOS machine.
return [ return {
data: [
{ {
name: this.getName(data.platform), name: this.getName(data.platform),
platform: "chrome", platform: "chrome",
@ -77,6 +78,7 @@ export default class TableOSVersion extends Table {
// https://developer.chrome.com/docs/extensions/reference/runtime/#type-PlatformArch // https://developer.chrome.com/docs/extensions/reference/runtime/#type-PlatformArch
arch: arch, arch: arch,
}, },
]; ],
};
} }
} }

View File

@ -5,13 +5,15 @@ export default class TableOsqueryInfo extends Table {
columns = ["version", "build_platform", "build_distro", "extensions"]; columns = ["version", "build_platform", "build_distro", "extensions"];
async generate() { async generate() {
return [ return {
data: [
{ {
version: `fleetd-chrome-${chrome.runtime.getManifest().version}`, version: `fleetd-chrome-${chrome.runtime.getManifest().version}`,
build_platform: "chrome", build_platform: "chrome",
build_distro: "chrome", build_distro: "chrome",
extensions: "inactive", extensions: "inactive",
}, },
]; ],
};
} }
} }

View File

@ -88,10 +88,12 @@ export default class TablePrivacyPreferences extends Table {
const columns = await Promise.all(results); const columns = await Promise.all(results);
errors.length > 0 && errors.length > 0 &&
console.log("Caught errors in chrome API calls: ", errors); console.log("Caught errors in chrome API calls: ", errors);
return [ return {
data: [
columns.reduce((resultRow, column) => { columns.reduce((resultRow, column) => {
return { ...resultRow, ...column }; return { ...resultRow, ...column };
}, {}), }, {}),
]; ],
};
} }
} }

View File

@ -7,11 +7,13 @@ describe("screenlock", () => {
const db = await VirtualDatabase.init(); const db = await VirtualDatabase.init();
const res = await db.query("select * from screenlock"); const res = await db.query("select * from screenlock");
expect(res).toEqual([ expect(res).toEqual({
data: [
{ {
enabled: 1, enabled: 1,
grace_period: 600, grace_period: 600,
}, },
]); ],
});
}); });
}); });

View File

@ -13,7 +13,7 @@ export default class TableScreenLock extends Table {
const enabled = delay > 0 ? "1" : "0"; const enabled = delay > 0 ? "1" : "0";
const gracePeriod = delay > 0 ? delay.toString() : "-1"; const gracePeriod = delay > 0 ? delay.toString() : "-1";
return [{ enabled, grace_period: gracePeriod }]; return { data: [{ enabled, grace_period: gracePeriod }] };
} }
throw new Error( throw new Error(
"Unexpected response from chrome.idle.getAutoLockDelay - expected number" "Unexpected response from chrome.idle.getAutoLockDelay - expected number"

View File

@ -2,7 +2,7 @@ import TableSystemInfo from "./system_info";
describe("system_info", () => { describe("system_info", () => {
describe("getComputerName", () => { describe("getComputerName", () => {
const sut = new TableSystemInfo(null, null) const sut = new TableSystemInfo(null, null);
it("is computed from the hostname and hw serial", () => { it("is computed from the hostname and hw serial", () => {
const testCases: [string, string, string][] = [ const testCases: [string, string, string][] = [
[null, null, "Chromebook"], [null, null, "Chromebook"],
@ -11,11 +11,13 @@ describe("system_info", () => {
["mychromebook", "", "mychromebook"], ["mychromebook", "", "mychromebook"],
["mychromebook", "123", "mychromebook"], ["mychromebook", "123", "mychromebook"],
["", "123", "Chromebook 123"], ["", "123", "Chromebook 123"],
] ];
for (let [hostname, hwSerial, expected] of testCases) { for (let [hostname, hwSerial, expected] of testCases) {
expect(sut.getComputerName(hostname, hwSerial)).toEqual(expected) expect(sut.getComputerName(hostname, hwSerial)).toEqual({
data: expected,
});
} }
}) });
}) });
}) });

View File

@ -29,6 +29,8 @@ export default class TableSystemInfo extends Table {
} }
async generate() { async generate() {
let warningsArray = [];
// @ts-expect-error @types/chrome doesn't yet have instanceID. // @ts-expect-error @types/chrome doesn't yet have instanceID.
const uuid = (await chrome.instanceID.getID()) as string; const uuid = (await chrome.instanceID.getID()) as string;
@ -39,14 +41,22 @@ export default class TableSystemInfo extends Table {
hostname = (await chrome.enterprise.deviceAttributes.getDeviceHostname()) as string; hostname = (await chrome.enterprise.deviceAttributes.getDeviceHostname()) as string;
} catch (err) { } catch (err) {
console.warn("get hostname:", err); console.warn("get hostname:", err);
warningsArray.push({
column: "hostname",
error_message: err.message.toString(),
});
} }
let hwSerial = ""; let hwSerial = "";
try { try {
// @ts-expect-error @types/chrome doesn't yet have the deviceAttributes Promise API. // @ts-expect-error @types/chrome doesn't yet have the deviceAttributes Promise API.
hwSerial = await chrome.enterprise.deviceAttributes.getDeviceSerialNumber(); hwSerial = (await chrome.enterprise.deviceAttributes.getDeviceSerialNumber()) as string;
} catch (err) { } catch (err) {
console.warn("get serial number:", err); console.warn("get serial number:", err);
warningsArray.push({
column: "hardware_serial",
error_message: err.message.toString(),
});
} }
let hwVendor = "", let hwVendor = "",
@ -61,6 +71,14 @@ export default class TableSystemInfo extends Table {
hwModel = platformInfo.model; hwModel = platformInfo.model;
} catch (err) { } catch (err) {
console.warn("get platform info:", err); console.warn("get platform info:", err);
warningsArray.push({
column: "hardware_vendor",
error_message: err.message.toString(),
});
warningsArray.push({
column: "hardware_model",
error_message: err.message.toString(),
});
} }
let cpuBrand = "", let cpuBrand = "",
@ -71,6 +89,14 @@ export default class TableSystemInfo extends Table {
cpuType = cpuInfo.archName; cpuType = cpuInfo.archName;
} catch (err) { } catch (err) {
console.warn("get cpu info:", err); console.warn("get cpu info:", err);
warningsArray.push({
column: "cpu_brand",
error_message: err.message.toString(),
});
warningsArray.push({
column: "cpu_type",
error_message: err.message.toString(),
});
} }
let physicalMemory = ""; let physicalMemory = "";
@ -79,9 +105,14 @@ export default class TableSystemInfo extends Table {
physicalMemory = memoryInfo.capacity.toString(); physicalMemory = memoryInfo.capacity.toString();
} catch (err) { } catch (err) {
console.warn("get memory info:", err); console.warn("get memory info:", err);
warningsArray.push({
column: "physical_memory",
error_message: err.message.toString(),
});
} }
return [ return {
data: [
{ {
uuid, uuid,
hostname, hostname,
@ -93,6 +124,8 @@ export default class TableSystemInfo extends Table {
cpu_type: cpuType, cpu_type: cpuType,
physical_memory: physicalMemory, physical_memory: physicalMemory,
}, },
]; ],
warnings: warningsArray,
};
} }
} }

View File

@ -8,10 +8,12 @@ describe("screenlock", () => {
const db = await VirtualDatabase.init(); const db = await VirtualDatabase.init();
const res = await db.query("select * from system_state"); const res = await db.query("select * from system_state");
expect(res).toEqual([ expect(res).toEqual({
data: [
{ {
idle_state: "active", idle_state: "active",
}, },
]); ],
});
}); });
}); });

View File

@ -24,6 +24,6 @@ export default class TableSystemState extends Table {
} }
); );
return [{ idle_state: idleState }]; return { data: [{ idle_state: idleState }] };
} }
} }

View File

@ -6,6 +6,6 @@ export default class TableUsers extends Table {
async generate() { async generate() {
const { email, id } = await chrome.identity.getProfileUserInfo({}); const { email, id } = await chrome.identity.getProfileUserInfo({});
return [{ uid: id, email, username: email }]; return { data: [{ uid: id, email, username: email }] };
} }
} }

View File

@ -4,7 +4,7 @@ import { IHost } from "./host";
export default PropTypes.shape({ export default PropTypes.shape({
hosts_count: PropTypes.shape({ hosts_count: PropTypes.shape({
total: PropTypes.number, total: PropTypes.number,
successful: PropTypes.number, successful: PropTypes.number, // Does not include ChromeOS results that are partially successful
failed: PropTypes.number, failed: PropTypes.number,
}), }),
id: PropTypes.number, id: PropTypes.number,
@ -26,7 +26,7 @@ export interface ICampaign {
hosts: IHost[]; hosts: IHost[];
hosts_count: { hosts_count: {
total: number; total: number;
successful: number; successful: number; // Does not include ChromeOS results that are partially successful
failed: number; failed: number;
}; };
id: number; id: number;

View File

@ -213,8 +213,7 @@ const QueryResults = ({
// TODO - clean up these conditions // TODO - clean up these conditions
const hasNoResultsYet = const hasNoResultsYet =
!isQueryFinished && (!queryResults?.length || tableHeaders === null); !isQueryFinished && (!queryResults?.length || tableHeaders === null);
const finishedWithNoResults = const finishedWithNoResults = isQueryFinished && !queryResults?.length;
isQueryFinished && (!queryResults?.length || !hostsCount.successful);
if (hasNoResultsYet) { if (hasNoResultsYet) {
return <AwaitingResults />; return <AwaitingResults />;

View File

@ -1,5 +1,4 @@
.query-results { .query-results {
&__results-cta { &__results-cta {
display: flex; display: flex;
} }
@ -52,4 +51,8 @@
&__error-table-container { &__error-table-container {
margin-top: 32px; margin-top: 32px;
} }
.error__cell {
white-space: pre-wrap; // Converts \n into new lines
}
} }

View File

@ -1079,7 +1079,7 @@ func (svc *Service) ingestQueryResults(
var err error var err error
switch { switch {
case strings.HasPrefix(query, hostDistributedQueryPrefix): case strings.HasPrefix(query, hostDistributedQueryPrefix):
err = svc.ingestDistributedQuery(ctx, *host, query, rows, failed, messages[query]) err = svc.ingestDistributedQuery(ctx, *host, query, rows, messages[query])
case strings.HasPrefix(query, hostPolicyQueryPrefix): case strings.HasPrefix(query, hostPolicyQueryPrefix):
err = ingestMembershipQuery(hostPolicyQueryPrefix, query, rows, policyResults, failed) err = ingestMembershipQuery(hostPolicyQueryPrefix, query, rows, policyResults, failed)
case strings.HasPrefix(query, hostLabelQueryPrefix): case strings.HasPrefix(query, hostLabelQueryPrefix):
@ -1147,7 +1147,7 @@ func (svc *Service) directIngestDetailQuery(ctx context.Context, host *fleet.Hos
// ingestDistributedQuery takes the results of a distributed query and modifies the // ingestDistributedQuery takes the results of a distributed query and modifies the
// provided fleet.Host appropriately. // provided fleet.Host appropriately.
func (svc *Service) ingestDistributedQuery(ctx context.Context, host fleet.Host, name string, rows []map[string]string, failed bool, errMsg string) error { func (svc *Service) ingestDistributedQuery(ctx context.Context, host fleet.Host, name string, rows []map[string]string, errMsg string) error {
trimmedQuery := strings.TrimPrefix(name, hostDistributedQueryPrefix) trimmedQuery := strings.TrimPrefix(name, hostDistributedQueryPrefix)
campaignID, err := strconv.Atoi(osquery_utils.EmptyToZero(trimmedQuery)) campaignID, err := strconv.Atoi(osquery_utils.EmptyToZero(trimmedQuery))
@ -1165,7 +1165,7 @@ func (svc *Service) ingestDistributedQuery(ctx context.Context, host fleet.Host,
}, },
Rows: rows, Rows: rows,
} }
if failed { if errMsg != "" {
res.Error = &errMsg res.Error = &errMsg
} }

View File

@ -1762,7 +1762,7 @@ func TestIngestDistributedQueryParseIdError(t *testing.T) {
} }
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "bad_name", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "bad_name", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "unable to parse campaign") assert.Contains(t, err.Error(), "unable to parse campaign")
} }
@ -1788,7 +1788,7 @@ func TestIngestDistributedQueryOrphanedCampaignLoadError(t *testing.T) {
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "loading orphaned campaign") assert.Contains(t, err.Error(), "loading orphaned campaign")
} }
@ -1821,7 +1821,7 @@ func TestIngestDistributedQueryOrphanedCampaignWaitListener(t *testing.T) {
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "campaignID=42 waiting for listener") assert.Contains(t, err.Error(), "campaignID=42 waiting for listener")
} }
@ -1857,7 +1857,7 @@ func TestIngestDistributedQueryOrphanedCloseError(t *testing.T) {
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "closing orphaned campaign") assert.Contains(t, err.Error(), "closing orphaned campaign")
} }
@ -1894,7 +1894,7 @@ func TestIngestDistributedQueryOrphanedStopError(t *testing.T) {
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "stopping orphaned campaign") assert.Contains(t, err.Error(), "stopping orphaned campaign")
} }
@ -1931,7 +1931,7 @@ func TestIngestDistributedQueryOrphanedStop(t *testing.T) {
host := fleet.Host{ID: 1} host := fleet.Host{ID: 1}
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "campaignID=42 stopped") assert.Contains(t, err.Error(), "campaignID=42 stopped")
lq.AssertExpectations(t) lq.AssertExpectations(t)
@ -1962,7 +1962,7 @@ func TestIngestDistributedQueryRecordCompletionError(t *testing.T) {
}() }()
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "record query completion") assert.Contains(t, err.Error(), "record query completion")
lq.AssertExpectations(t) lq.AssertExpectations(t)
@ -1993,7 +1993,7 @@ func TestIngestDistributedQuery(t *testing.T) {
}() }()
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, false, "") err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "")
require.NoError(t, err) require.NoError(t, err)
lq.AssertExpectations(t) lq.AssertExpectations(t)
} }