Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/locales/gsa-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"Error Messages": "Fehlermeldungen",
"Error while loading Container Scanning Results for Report {{reportId}}": "Fehler beim Laden der Container-Scan-Ergebnisse für Bericht {{reportId}}",
"Error while loading Errors for Report {{reportId}}": "Fehler beim Laden der Fehler für Bericht {{reportId}}",
"Error while loading Operating Systems for Report {{reportId}}": "Fehler beim Laden der Betriebssysteme für Bericht {{reportId}}",
"Error while loading Ports for Report {{reportId}}": "Fehler beim Laden der Ports für Bericht {{reportId}}",
"Error while loading Report {{reportId}}": "Fehler beim Laden des Berichts {{reportId}}",
"Error while loading Results for Report {{reportId}}": "Fehler beim Laden von Ergebnissen des Berichts {{reportId}}",
Expand Down
1 change: 1 addition & 0 deletions public/locales/gsa-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"Error Messages": "Error Messages",
"Error while loading Container Scanning Results for Report {{reportId}}": "",
"Error while loading Errors for Report {{reportId}}": "",
"Error while loading Operating Systems for Report {{reportId}}": "",
"Error while loading Ports for Report {{reportId}}": "",
"Error while loading Report {{reportId}}": "Error while loading Report {{reportId}}",
"Error while loading Results for Report {{reportId}}": "Error while loading Results for Report {{reportId}}",
Expand Down
1 change: 1 addition & 0 deletions public/locales/gsa-zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"Error Messages": "错误消息",
"Error while loading Container Scanning Results for Report {{reportId}}": "",
"Error while loading Errors for Report {{reportId}}": "",
"Error while loading Operating Systems for Report {{reportId}}": "",
"Error while loading Ports for Report {{reportId}}": "",
"Error while loading Report {{reportId}}": "加载报告 {{reportId}} 时出错",
"Error while loading Results for Report {{reportId}}": "加载报告 {{reportId}} 的结果时出错",
Expand Down
1 change: 1 addition & 0 deletions public/locales/gsa-zh_TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"Error Messages": "錯誤訊息",
"Error while loading Container Scanning Results for Report {{reportId}}": "",
"Error while loading Errors for Report {{reportId}}": "",
"Error while loading Operating Systems for Report {{reportId}}": "",
"Error while loading Ports for Report {{reportId}}": "",
"Error while loading Report {{reportId}}": "",
"Error while loading Results for Report {{reportId}}": "",
Expand Down
109 changes: 109 additions & 0 deletions src/gmp/commands/__tests__/report-operating-system.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* SPDX-FileCopyrightText: 2026 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect} from '@gsa/testing';
import ReportOperatingSystemsCommand from 'gmp/commands/report-operating-system';
import {createHttp, createResponse} from 'gmp/commands/testing';

const makeResponse = (operatingSystems: object) =>
createResponse({
get_report_operating_systems: {
get_report_operating_systems_response: {
operating_systems: operatingSystems,
},
},
});

describe('ReportOperatingSystemsCommand tests', () => {
test('should fetch and parse multiple OS entities', async () => {
const response = makeResponse({
operating_system: [
{best_os_cpe: 'cpe:/foo/bar', best_os_txt: 'Foo OS', hosts_count: '2'},
{
best_os_cpe: 'cpe:/lorem/ipsum',
best_os_txt: 'Lorem OS',
hosts_count: '5',
},
],
});
const fakeHttp = createHttp(response);
const cmd = new ReportOperatingSystemsCommand(fakeHttp);

const result = await cmd.get({report_id: '1234'});

expect(fakeHttp.request).toHaveBeenCalledWith('get', {
args: {
cmd: 'get_report_operating_systems',
details: 1,
report_id: '1234',
},
});

expect(result.data).toHaveLength(2);
expect(result.data[0].cpe).toBe('cpe:/foo/bar');
expect(result.data[0].name).toBe('Foo OS');
expect(result.data[0].hosts.count).toBe(2);
expect(result.data[1].cpe).toBe('cpe:/lorem/ipsum');
expect(result.data[1].name).toBe('Lorem OS');
expect(result.data[1].hosts.count).toBe(5);
});

test('should fetch and parse a single OS entity (non-array from XML)', async () => {
const response = makeResponse({
operating_system: {
best_os_cpe: 'cpe:/single/os',
best_os_txt: 'Single OS',
hosts_count: '3',
},
});
const fakeHttp = createHttp(response);
const cmd = new ReportOperatingSystemsCommand(fakeHttp);

const result = await cmd.get({report_id: '1234'});

expect(result.data).toHaveLength(1);
expect(result.data[0].cpe).toBe('cpe:/single/os');
expect(result.data[0].name).toBe('Single OS');
expect(result.data[0].hosts.count).toBe(3);
});

test('should return correct CollectionCounts', async () => {
const response = makeResponse({
operating_system: [
{best_os_cpe: 'cpe:/a', best_os_txt: 'OS A', hosts_count: '1'},
{best_os_cpe: 'cpe:/b', best_os_txt: 'OS B', hosts_count: '10'},
],
});
const fakeHttp = createHttp(response);
const cmd = new ReportOperatingSystemsCommand(fakeHttp);

const result = await cmd.get({report_id: '1234'});

expect(result.meta.counts.all).toBe(2);
expect(result.meta.counts.filtered).toBe(2);
expect(result.meta.counts.length).toBe(2);
});

test('should handle empty operating_systems', async () => {
const response = makeResponse({});
const fakeHttp = createHttp(response);
const cmd = new ReportOperatingSystemsCommand(fakeHttp);

const result = await cmd.get({report_id: '1234'});

expect(result.data).toHaveLength(0);
expect(result.meta.counts.all).toBe(0);
});

test('should throw when response wrapper is missing', async () => {
const response = createResponse({});
const fakeHttp = createHttp(response);
const cmd = new ReportOperatingSystemsCommand(fakeHttp);

await expect(cmd.get({report_id: '1234'})).rejects.toThrow(
'Invalid response: get_report_operating_systems not found in response',
);
});
});
92 changes: 92 additions & 0 deletions src/gmp/commands/report-operating-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* SPDX-FileCopyrightText: 2026 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import CollectionCounts from 'gmp/collection/collection-counts';
import {parseFilter} from 'gmp/collection/parser';
import type {EntitiesMeta} from 'gmp/commands/entities';
import HttpCommand, {
type HttpCommandInputParams,
type HttpCommandOptions,
} from 'gmp/commands/http';
import type Http from 'gmp/http/http';
import type Response from 'gmp/http/response';
import type {XmlResponseData} from 'gmp/http/transform/fast-xml';
import type {FilterModelElement} from 'gmp/models/filter';
import ReportOperatingSystem from 'gmp/models/report/os';
import {map} from 'gmp/utils/array';

interface OperatingSystemElement {
best_os_cpe?: string;
best_os_txt?: string;
hosts_count?: string | number;
}

interface ReportOperatingSystemsResponseData extends XmlResponseData {
get_report_operating_systems?: {
get_report_operating_systems_response?: {
filters?: FilterModelElement;
operating_systems?: {
operating_system?: OperatingSystemElement | OperatingSystemElement[];
};
};
};
}

class ReportOperatingSystemsCommand extends HttpCommand {
constructor(http: Http) {
super(http, {cmd: 'get_report_operating_systems'});
}

async get(
params: HttpCommandInputParams = {},
options?: HttpCommandOptions,
): Promise<Response<ReportOperatingSystem[], EntitiesMeta>> {
const response = await this.httpGetWithTransform(
{details: 1, ...params},
options,
);

const root = response.data as ReportOperatingSystemsResponseData;

if (!root.get_report_operating_systems) {
throw new Error(
'Invalid response: get_report_operating_systems not found in response',
);
}

const data =
root.get_report_operating_systems.get_report_operating_systems_response;

const filter = parseFilter(data ?? {});

const entities = map(
data?.operating_systems?.operating_system,
(item: OperatingSystemElement) => {
const os = ReportOperatingSystem.fromElement({
best_os_cpe: item.best_os_cpe,
best_os_txt: item.best_os_txt,
});
os.hosts.count = Number(item.hosts_count) || 0;
return os;
},
);

const filteredCount = entities.length;
const counts = new CollectionCounts({
all: filteredCount,
filtered: filteredCount,
first: 1,
length: filteredCount,
rows: filteredCount,
});

return response.set<ReportOperatingSystem[], EntitiesMeta>(entities, {
filter,
counts,
});
}
}

export default ReportOperatingSystemsCommand;
3 changes: 3 additions & 0 deletions src/gmp/gmp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import ReportConfigsCommand from 'gmp/commands/report-configs';
import ReportsErrorsCommand from 'gmp/commands/report-errors';
import ReportFormatCommand from 'gmp/commands/report-format';
import ReportFormatsCommand from 'gmp/commands/report-formats';
import ReportOperatingSystemsCommand from 'gmp/commands/report-operating-system';
import ReportPortsCommand from 'gmp/commands/report-ports';
import ReportTlsCertificatesCommand from 'gmp/commands/report-tls-certificates';
import ReportsCommand from 'gmp/commands/reports';
Expand Down Expand Up @@ -150,6 +151,7 @@ class Gmp {
public readonly reportformats: ReportFormatsCommand;
public readonly reports: ReportsCommand;
public readonly reportports: ReportPortsCommand;
public readonly reportoperatingsystems: ReportOperatingSystemsCommand;
public readonly reporttlscertificates: ReportTlsCertificatesCommand;
public readonly result: ResultCommand;
public readonly results: ResultsCommand;
Expand Down Expand Up @@ -239,6 +241,7 @@ class Gmp {
this.reporterrors = new ReportsErrorsCommand(this.http);
this.reportformat = new ReportFormatCommand(this.http);
this.reportformats = new ReportFormatsCommand(this.http);
this.reportoperatingsystems = new ReportOperatingSystemsCommand(this.http);
this.reports = new ReportsCommand(this.http);
this.reportports = new ReportPortsCommand(this.http);
this.reporttlscertificates = new ReportTlsCertificatesCommand(this.http);
Expand Down
117 changes: 117 additions & 0 deletions src/web/hooks/use-query/__tests__/ReportOperatingSystem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2026 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect, testing} from '@gsa/testing';
import {rendererWith, screen, waitFor} from 'web/testing';
import CollectionCounts from 'gmp/collection/collection-counts';
import Filter from 'gmp/models/filter';
import ReportOperatingSystem from 'gmp/models/report/os';
import {createSession} from 'gmp/testing';
import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity';
import {useGetReportOperatingSystems} from 'web/hooks/use-query/report-operating-system';

const os1 = ReportOperatingSystem.fromElement({
best_os_cpe: 'cpe:/foo/bar',
best_os_txt: 'Foo OS',
});
os1.hosts.count = 2;

const os2 = ReportOperatingSystem.fromElement({
best_os_cpe: 'cpe:/lorem/ipsum',
best_os_txt: 'Lorem OS',
});
os2.hosts.count = 5;

const filter = Filter.fromString('rows=10 first=1');

const TestComponent = ({
reportId,
filter: testFilter,
}: {
reportId: string;
filter?: Filter;
}) => {
const {data, isLoading, isError} = useGetReportOperatingSystems({
reportId,
filter: testFilter,
});

if (isLoading) {
return <div data-testid="loading">Loading...</div>;
}
if (isError) {
return <div data-testid="error">Error</div>;
}
if (!data) {
return <div data-testid="no-data">No data</div>;
}

return (
<div data-testid="entities">
{data.entities.map(os => (
<div key={os.cpe} data-testid="os-entity">
{os.name}
</div>
))}
</div>
);
};

const createGmp = () => ({
session: createSession({token: 'test-token'}),
settings: {severityRating: SEVERITY_RATING_CVSS_3},
reportoperatingsystems: {
get: testing.fn().mockResolvedValue({
data: [os1, os2],
meta: {
filter,
counts: new CollectionCounts({all: 2, filtered: 2, length: 2}),
},
}),
},
});

describe('useGetReportOperatingSystems', () => {
test('should fetch OS entities for a given reportId', async () => {
const gmp = createGmp();
const {render} = rendererWith({gmp, router: true});

render(<TestComponent filter={filter} reportId="1234" />);

await waitFor(() => {
expect(screen.getAllByTestId('os-entity')).toHaveLength(2);
});

expect(gmp.reportoperatingsystems.get).toHaveBeenCalledWith(
expect.objectContaining({report_id: '1234'}),
);
expect(screen.getByText('Foo OS')).toBeInTheDocument();
expect(screen.getByText('Lorem OS')).toBeInTheDocument();
});

test('should show loading state initially', () => {
const gmp = createGmp();
// Replace with a promise that never resolves to keep loading state
gmp.reportoperatingsystems.get = testing
.fn()
.mockReturnValue(new Promise(() => {}));

const {render} = rendererWith({gmp, router: true});
render(<TestComponent filter={filter} reportId="1234" />);

expect(screen.getByTestId('loading')).toBeInTheDocument();
});

test('should not fetch when reportId is empty', () => {
const gmp = createGmp();
const {render} = rendererWith({gmp, router: true});

render(<TestComponent filter={filter} reportId="" />);

// Query is disabled when reportId is empty — no fetch is triggered
expect(screen.getByTestId('no-data')).toBeInTheDocument();
expect(gmp.reportoperatingsystems.get).not.toHaveBeenCalled();
});
});
3 changes: 3 additions & 0 deletions src/web/hooks/use-query/report-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import useGetEntities from 'web/queries/useGetEntities';
interface UseGetReportErrorsParams {
reportId: string;
filter?: Filter;
refetchInterval?: number | false;
}

export const useGetReportErrors = ({
reportId,
filter = undefined,
refetchInterval = undefined,
}: UseGetReportErrorsParams) => {
const gmp = useGmp();

Expand All @@ -28,6 +30,7 @@ export const useGetReportErrors = ({
filter,
enabled: Boolean(reportId),
keepPreviousData: true,
refetchInterval,
});
};

Expand Down
Loading
Loading