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
73 changes: 73 additions & 0 deletions apps/api/src/policies/dto/create-policy.dto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ValidationPipe } from '@nestjs/common';
import { CreatePolicyDto } from './create-policy.dto';
import { UpdatePolicyDto } from './update-policy.dto';

/**
* Regression test for the MCP/public-API policy content serialization bug.
*
* The global ValidationPipe in main.ts runs with `transform: true` and
* `transformOptions: { enableImplicitConversion: true }`. class-transformer
* then coerces each TipTap node object toward the reflected `Array` design-type
* of `content: unknown[]`, turning `[{...}, {...}]` into `[[], []]` — silently
* blanking the policy body on create/update. These tests exercise the exact
* pipe configuration and assert the structured content survives intact.
*/
const pipe = new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
});

const TIPTAP_NODES = [
{
type: 'heading',
attrs: { level: 2, textAlign: null },
content: [{ type: 'text', text: 'Purpose' }],
},
{
type: 'paragraph',
attrs: { textAlign: null },
content: [
{
type: 'text',
text: 'Verify workforce integrity and grant the right access.',
marks: [{ type: 'bold' }],
},
],
},
];

describe('Policy DTO content serialization (ValidationPipe)', () => {
it('preserves structured TipTap content on CreatePolicyDto', async () => {
const result = await pipe.transform(
{ name: 'Access Control Policy', content: TIPTAP_NODES },
{ type: 'body', metatype: CreatePolicyDto },
);

expect(result.content).toEqual(TIPTAP_NODES);
// Guard against the exact regression: nodes must not collapse to `[]`.
expect(result.content[0]).toMatchObject({ type: 'heading' });
expect(result.content).not.toEqual([[], []]);
});

it('preserves structured TipTap content on UpdatePolicyDto', async () => {
const result = await pipe.transform(
{ content: TIPTAP_NODES },
{ type: 'body', metatype: UpdatePolicyDto },
);

expect(result.content).toEqual(TIPTAP_NODES);
expect(result.content[1]).toMatchObject({ type: 'paragraph' });
expect(result.content).not.toEqual([[], []]);
});

it('leaves content untouched when omitted on update', async () => {
const result = await pipe.transform(
{ name: 'Renamed only' },
{ type: 'body', metatype: UpdatePolicyDto },
);

expect(result.content).toBeUndefined();
});
});
7 changes: 6 additions & 1 deletion apps/api/src/policies/dto/create-policy.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export class CreatePolicyDto {
items: { type: 'object', additionalProperties: true },
})
@IsArray()
@Transform(({ value }) => value)
// Return the raw source value. Under the global ValidationPipe's implicit
// conversion, class-transformer coerces each TipTap node toward the reflected
// Array design-type of `content`, mangling `[{...}, {...}]` into `[[], []]`.
// The transform runs after that coercion, so `value` is already mangled —
// `obj.content` is the untouched original. Do not revert this to `value`.
@Transform(({ obj }) => obj.content)
content: unknown[];

@ApiProperty({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,63 +248,95 @@ describe('DeviceAgentDevicesList — integration-imported devices', () => {
expect(screen.getByTitle('Offline')).toBeInTheDocument();
});

it('shows the SOURCE-reported verdict and named checks when the provider reports them', () => {
it('shows Yes only when all four canonical checks pass (extras are display-only)', () => {
render(
<DeviceAgentDevicesList
devices={[
makeIntegrationDevice({
integrationProvider: { slug: 'intune', name: 'Microsoft Intune' },
sourceCompliance: {
isCompliant: true,
checks: [
{ id: 'disk_encryption', label: 'Disk Encryption', passed: true },
{ id: 'antivirus', label: 'Antivirus', passed: true },
{ id: 'password_policy', label: 'Password Policy', passed: true },
{ id: 'screen_lock', label: 'Screen Lock', passed: true },
// Non-canonical: shown as a badge but never affects the verdict.
{ id: 'firewall', label: 'Firewall', passed: false },
],
},
}),
]}
/>,
);
// Verdict shown instead of "Not tracked", with provider attribution.
expect(screen.getByText('Yes')).toBeInTheDocument();
expect(screen.queryByText('Not tracked')).not.toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Where does this compliance status come from\?/i }),
).toBeInTheDocument();
// Provider-named check badges, pass and fail.
expect(screen.queryByText(/Unverified/)).not.toBeInTheDocument();
// Provider-named check badges, incl. the informational extra.
expect(screen.getByText('Disk Encryption')).toBeInTheDocument();
expect(screen.getByText('Firewall')).toBeInTheDocument();
});

it('shows "No" when the source reports non-compliant', () => {
it('shows "No" when a canonical check fails, even if the vendor claims compliant (showcase bug)', () => {
render(
<DeviceAgentDevicesList
devices={[
makeIntegrationDevice({
sourceCompliance: { isCompliant: false },
sourceCompliance: {
// Intune with no policies configured calls everything compliant —
// CompAI's framework standard (the four checks) must overrule it.
isCompliant: true,
checks: [
{ id: 'disk_encryption', label: 'Disk Encryption', passed: false },
],
},
}),
]}
/>,
);
expect(screen.getByText('No')).toBeInTheDocument();
expect(screen.queryByText('Not tracked')).not.toBeInTheDocument();
expect(screen.queryByText('Yes')).not.toBeInTheDocument();
expect(screen.queryByText(/Unverified/)).not.toBeInTheDocument();
});

it('shows "Unverified (n/4)" when only some canonical checks are reported and none fail', () => {
render(
<DeviceAgentDevicesList
devices={[
makeIntegrationDevice({
sourceCompliance: {
checks: [
{ id: 'disk_encryption', label: 'Disk Encryption', passed: true },
{ id: 'antivirus', label: 'Antivirus', passed: true },
],
},
}),
]}
/>,
);
expect(screen.getByText('Unverified (2/4)')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Why is compliance unverified\?/i }),
).toBeInTheDocument();
expect(screen.queryByText('Yes')).not.toBeInTheDocument();
expect(screen.queryByText('No')).not.toBeInTheDocument();
});

it('keeps "Not tracked" when the source reports checks but no verdict', () => {
it('keeps "Not tracked" when the source reports no canonical checks at all', () => {
render(
<DeviceAgentDevicesList
devices={[
makeIntegrationDevice({
sourceCompliance: {
// A vendor verdict alone is NOT evidence against our standard.
isCompliant: true,
checks: [{ id: 'os_up_to_date', label: 'OS up to date', passed: true }],
},
}),
]}
/>,
);
// Checks render, but with no overall verdict the compliance column stays honest.
expect(screen.getByText('OS up to date')).toBeInTheDocument();
expect(screen.getByText('Not tracked')).toBeInTheDocument();
expect(screen.queryByText('Yes')).not.toBeInTheDocument();
});

it('renders a source filter only when more than one source is present', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ describe('DeviceComplianceChart', () => {
expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
});

it('does not show a Not Tracked legend entry when every device has a verdict', () => {
it('does not show an Unverified legend entry when every device has a verdict', () => {
render(
<DeviceComplianceChart fleetDevices={[]} agentDevices={[makeAgentDevice()]} />,
);
expect(screen.queryByText(/Not Tracked/)).not.toBeInTheDocument();
expect(screen.queryByText(/Unverified/)).not.toBeInTheDocument();
});
});

Expand All @@ -244,32 +244,49 @@ function makeIntegrationDevice(
}

describe('DeviceComplianceChart — integration-imported devices', () => {
it('counts an imported device with a source verdict of compliant as Compliant', () => {
it('counts an imported device with all four canonical checks passing as Compliant', () => {
render(
<DeviceComplianceChart
fleetDevices={[]}
agentDevices={[
makeAgentDevice({ isCompliant: false, complianceStatus: 'non_compliant' }),
makeIntegrationDevice({ sourceCompliance: { isCompliant: true } }),
makeIntegrationDevice({
sourceCompliance: {
checks: [
{ id: 'disk_encryption', label: 'Disk Encryption', passed: true },
{ id: 'antivirus', label: 'Antivirus', passed: true },
{ id: 'password_policy', label: 'Password Policy', passed: true },
{ id: 'screen_lock', label: 'Screen Lock', passed: true },
],
},
}),
]}
/>,
);
expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
expect(screen.getByText('Non-Compliant (1)')).toBeInTheDocument();
expect(screen.queryByText(/Not Tracked/)).not.toBeInTheDocument();
expect(screen.queryByText(/Unverified/)).not.toBeInTheDocument();
});

it('counts an imported device with a source verdict of non-compliant as Non-Compliant', () => {
it('counts an imported device with a failed canonical check as Non-Compliant (vendor verdict ignored)', () => {
render(
<DeviceComplianceChart
fleetDevices={[]}
agentDevices={[makeIntegrationDevice({ sourceCompliance: { isCompliant: false } })]}
agentDevices={[
makeIntegrationDevice({
sourceCompliance: {
// Vendor says compliant — CompAI's canonical standard overrules.
isCompliant: true,
checks: [{ id: 'disk_encryption', label: 'Disk Encryption', passed: false }],
},
}),
]}
/>,
);
expect(screen.getByText('Non-Compliant (1)')).toBeInTheDocument();
});

it('puts an imported device with no source verdict into a Not Tracked segment', () => {
it('puts an imported device with partial/no canonical checks into an Unverified segment', () => {
render(
<DeviceComplianceChart
fleetDevices={[]}
Expand All @@ -281,14 +298,25 @@ describe('DeviceComplianceChart — integration-imported devices', () => {
);
expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
expect(screen.getByText('Non-Compliant (0)')).toBeInTheDocument();
expect(screen.getByText('Not Tracked (1)')).toBeInTheDocument();
expect(screen.getByText('Unverified (1)')).toBeInTheDocument();
});

it('renders the chart (not the empty state) for an org with ONLY imported devices', () => {
render(
<DeviceComplianceChart
fleetDevices={[]}
agentDevices={[makeIntegrationDevice({ sourceCompliance: { isCompliant: true } })]}
agentDevices={[
makeIntegrationDevice({
sourceCompliance: {
checks: [
{ id: 'disk_encryption', label: 'Disk Encryption', passed: true },
{ id: 'antivirus', label: 'Antivirus', passed: true },
{ id: 'password_policy', label: 'Password Policy', passed: true },
{ id: 'screen_lock', label: 'Screen Lock', passed: true },
],
},
}),
]}
/>,
);
expect(screen.queryByText(/No device data available/)).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import * as React from 'react';
import { Cell, Label, Pie, PieChart } from 'recharts';
import type { DeviceWithChecks, Host } from '../types';
import { computeSourceComplianceVerdict } from '../lib/device-source';

interface DeviceComplianceChartProps {
fleetDevices: Host[];
Expand All @@ -23,14 +24,15 @@ interface DeviceComplianceChartProps {
const CHART_COLORS = {
compliant: 'var(--primary)', // brand green
nonCompliant: 'var(--destructive)', // red
notTracked: 'var(--muted-foreground)', // gray — no compliance data available
unverified: 'var(--muted-foreground)', // gray — canonical checks not (fully) reported
};

export function DeviceComplianceChart({ fleetDevices, agentDevices }: DeviceComplianceChartProps) {
// ALL devices count — the chart total must match the table so the page never
// looks self-contradictory. Imported devices use the SOURCE's verdict when
// it reports one; imported devices with no verdict land in a gray
// "Not tracked" segment rather than being fabricated into a verdict.
// looks self-contradictory. Imported devices are judged by CompAI's OWN
// standard (the four canonical checks, computed from source-reported data);
// partially/un-reported devices land in a gray "Unverified" segment rather
// than being fabricated into a verdict.
const devices = [...(agentDevices ?? []), ...(fleetDevices ?? [])];

const { pieDisplayData, legendDisplayData } = React.useMemo(() => {
Expand All @@ -39,15 +41,18 @@ export function DeviceComplianceChart({ fleetDevices, agentDevices }: DeviceComp
}
let compliantCount = 0;
let nonCompliantCount = 0;
let notTrackedCount = 0;
let unverifiedCount = 0;

for (const device of agentDevices ?? []) {
if (device.source === 'integration') {
// Source-reported verdict (e.g. Intune complianceState), if any.
const verdict = device.sourceCompliance?.isCompliant;
if (verdict === true) compliantCount++;
else if (verdict === false) nonCompliantCount++;
else notTrackedCount++;
// CompAI's verdict computed from the canonical source-reported checks
// (vendor's own verdict is informational-only and never counted).
const verdict = computeSourceComplianceVerdict(device);
if (verdict?.kind === 'compliant') compliantCount++;
else if (verdict?.kind === 'non_compliant') nonCompliantCount++;
// Both partial ('unverified') and zero-data ('not_tracked') devices
// land in the gray segment: neither has a defensible verdict.
else unverifiedCount++;
continue;
}
// Device-agent devices. Stale devices (no check-in for >= 7 days)
Expand Down Expand Up @@ -81,17 +86,17 @@ export function DeviceComplianceChart({ fleetDevices, agentDevices }: DeviceComp
fill: CHART_COLORS.nonCompliant,
},
{
name: 'Not Tracked',
value: notTrackedCount,
fill: CHART_COLORS.notTracked,
name: 'Unverified',
value: unverifiedCount,
fill: CHART_COLORS.unverified,
},
];
return {
pieDisplayData: allItems.filter((item) => item.value > 0),
// "Not Tracked" only appears in the legend when it exists — orgs with no
// verdict-less imported devices keep the familiar two-entry legend.
// "Unverified" only appears in the legend when it exists — orgs with no
// partially-verified imported devices keep the familiar two-entry legend.
legendDisplayData: allItems.filter(
(item) => item.name !== 'Not Tracked' || item.value > 0,
(item) => item.name !== 'Unverified' || item.value > 0,
),
};
}, [agentDevices, fleetDevices]);
Expand Down
Loading
Loading