Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 10 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,17 @@ func (c *Frontend) GetAggregateKeyLabels() map[string][]string {
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
}
const tlsVersionAggSuffix = "__TLSVersion"
for i := range c.Scopes {
keyLabels[c.Scopes[i].ID] = c.Scopes[i].Labels
id := c.Scopes[i].ID
labs := c.Scopes[i].Labels
keyLabels[id] = labs
// Same src/dst dimensions as topology scope, plus TLSVersion and TLSGroup (per-link TLS breakdown).
// TLSTypes is omitted from aggregation (cardinality); TLSGroup is used for PQC-oriented hints.
withTLS := make([]string, len(labs), len(labs)+2)
copy(withTLS, labs)
withTLS = append(withTLS, "TLSVersion", "TLSGroup")
keyLabels[id+tlsVersionAggSuffix] = withTLS
}
return keyLabels
}
42 changes: 32 additions & 10 deletions pkg/loki/topology_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ var lokiConfig = config.Loki{
}

var aggregateKeyLabels = map[string][]string{
"app": {"app"},
"droppedState": {"PktDropLatestState"},
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
"cluster": {"K8S_ClusterName"},
"zone": {"SrcK8S_Zone", "DstK8S_Zone"},
"host": {"SrcK8S_HostName", "DstK8S_HostName"},
"namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"},
"owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"},
"resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"},
"app": {"app"},
"droppedState": {"PktDropLatestState"},
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
"cluster": {"K8S_ClusterName"},
"zone": {"SrcK8S_Zone", "DstK8S_Zone"},
"host": {"SrcK8S_HostName", "DstK8S_HostName"},
"namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"},
"owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"},
"owner__TLSVersion": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace", "TLSVersion", "TLSGroup"},
"resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"},
}

func TestBuildTopologyQuery_SimpleAggregate(t *testing.T) {
Expand Down Expand Up @@ -123,3 +124,24 @@ func TestBuildTopologyQuery_CustomLabelAggregate(t *testing.T) {
result,
)
}

func TestBuildTopologyQuery_TlsFlowsOwnerPlusTlsVersionAggregate(t *testing.T) {
in := TopologyInput{
Start: "(start)",
End: "",
Top: "50",
RateInterval: "2m",
Step: "10s",
DataField: constants.MetricTypeTLSFlows,
MetricFunction: constants.MetricFunctionRate,
RecordType: constants.RecordTypeLog,
DataSource: constants.DataSourceAuto,
Aggregate: "owner__TLSVersion",
}
q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in)
require.NoError(t, err)
result := q.Build()
assert.Contains(t, result, "sum by(SrcK8S_OwnerName,SrcK8S_OwnerType,DstK8S_OwnerName,DstK8S_OwnerType,SrcK8S_Namespace,DstK8S_Namespace,TLSVersion,TLSGroup)")
assert.Contains(t, result, "|=\"TLSTypes\"")
assert.Contains(t, result, "|json")
}
17 changes: 17 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
"Cluster name": "Cluster name",
"UDN": "UDN",
"Can't find metrics for this element. Check your capture filters to ensure we can monitor it. Else it probably means there is no traffic here.": "Can't find metrics for this element. Check your capture filters to ensure we can monitor it. Else it probably means there is no traffic here.",
"Clear filter for {{value}}": "Clear filter for {{value}}",
"Apply filter for {{value}}": "Apply filter for {{value}}",
"TLS": "TLS",
"TLS-encrypted traffic was observed on this link for the selected scope and time range.": "TLS-encrypted traffic was observed on this link for the selected scope and time range.",
"TLS versions": "TLS versions",
"TLS groups": "TLS groups",
"TLS message types": "TLS message types",
"No TLS message type breakdown is available for this aggregation.": "No TLS message type breakdown is available for this aggregation.",
"Endpoint A": "Endpoint A",
"Source": "Source",
"Endpoint B": "Endpoint B",
Expand Down Expand Up @@ -158,6 +166,8 @@
"Show": "Show",
"Edges": "Edges",
"Edges label": "Edges label",
"Marks volume edges as cleartext when aggregated logs show no TLS signals, using an open-lock icon. Can add many icons; use for focused troubleshooting.": "Marks volume edges as cleartext when aggregated logs show no TLS signals, using an open-lock icon. Can add many icons; use for focused troubleshooting.",
"Cleartext traffic": "Cleartext traffic",
"Badges": "Badges",
"Empty": "Empty",
"Collapse groups": "Collapse groups",
Expand Down Expand Up @@ -545,6 +555,13 @@
"Show NoError responses grouped in a separate series": "Show NoError responses grouped in a separate series",
"Show NoError": "Show NoError",
"Export panel": "Export panel",
"No TLS signals in aggregated flow logs for this link (cleartext or not classified as TLS).": "No TLS signals in aggregated flow logs for this link (cleartext or not classified as TLS).",
"Observed TLS uses deprecated protocol versions (e.g. TLS 1.0 / 1.1 or SSL).": "Observed TLS uses deprecated protocol versions (e.g. TLS 1.0 / 1.1 or SSL).",
"Observed TLS includes TLS 1.2 (older but still common). Prefer TLS 1.3 where possible.": "Observed TLS includes TLS 1.2 (older but still common). Prefer TLS 1.3 where possible.",
"Observed TLS includes TLS 1.3 (current recommended).": "Observed TLS includes TLS 1.3 (current recommended).",
"Observed TLS 1.3 with a post-quantum key exchange group (PQC).": "Observed TLS 1.3 with a post-quantum key exchange group (PQC).",
"Observed TLS on this link; protocol version could not be classified from labels.": "Observed TLS on this link; protocol version could not be classified from labels.",
"Observed TLS on this link.": "Observed TLS on this link.",
"Step into this {{name}}": "Step into this {{name}}",
"Filter by source or destination {{name}}": "Filter by source or destination {{name}}",
"Filter by {{name}}": "Filter by {{name}}",
Expand Down
2 changes: 2 additions & 0 deletions web/src/api/ipfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export interface Flow {
_IsFirst?: string;
numFlowLogs?: number;
UdnId?: string;
TLSTypes?: string[];
TLSVersion?: string;
}

export enum FlowDirection {
Expand Down
12 changes: 12 additions & 0 deletions web/src/api/loki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,21 @@ export interface TopologyMetricPeer {
subnetLabel?: string;
}

/** TLS breakdown from Loki matrix metric labels when present (topology TLS aggregate). */
export type GenericMetricTls = {
/** Present on flow records; omitted from topology aggregation (cardinality). */
types?: string[];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types can be removed I guess?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 3ab6360

I initially kept that in the UI just in case but it seems we are going to a direction where it will never be the case.
We can reintroduce it if needed in the future (maybe if we introduce a side panel for overview metrics ?)

versions?: string[];
/** Cipher / key-exchange group (PQC compliance). */
groups?: string[];
};

export type GenericMetric = {
name: string;
values: [number, number][];
stats: MetricStats;
aggregateBy: Field;
tls?: GenericMetricTls;
};

export type FunctionMetrics = {
Expand Down Expand Up @@ -198,6 +208,8 @@ export type TopologyMetrics = {
values: [number, number][];
stats: MetricStats;
scope: FlowScope;
/** TLSVersion / TLSGroup from Loki topology matrix labels when present. */
tls?: GenericMetricTls;
};

export type NamedMetric = TopologyMetrics & {
Expand Down
57 changes: 57 additions & 0 deletions web/src/components/drawer/element/__tests__/element-panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,63 @@ describe('<ElementPanel />', () => {
expect(container.querySelector('#destination-content')?.textContent).toContain('172.30.0.10');
});

it('should show TLS block on edge when TLS data is present', async () => {
const edge = getEdge();
edge.setData({ tagTlsSecure: true, tlsTypeLabels: ['ClientHello', 'AppData'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('ClientHello');
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('AppData');
});

it('should show TLS block on edge when only tlsTypeLabels are set', async () => {
const edge = getEdge();
edge.setData({ tlsTypeLabels: ['ClientHello', 'AppData'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('ClientHello');
});

it('should list TLS versions on edge when tlsVersionLabels are set', async () => {
const edge = getEdge();
edge.setData({ tagTlsSecure: true, tlsVersionLabels: ['TLS 1.3', 'TLS 1.2'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('TLS 1.3');
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('TLS 1.2');
});

it('should list TLS groups on edge when tlsGroupLabels are set', async () => {
const edge = getEdge();
edge.setData({
tagTlsSecure: true,
tlsVersionLabels: ['TLS 1.3'],
tlsGroupLabels: ['X25519MLKEM768', 'X25519']
});
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('X25519MLKEM768');
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('X25519');
});

it('should show TLS block on edge when only tlsGroupLabels are set', async () => {
const edge = getEdge();
edge.setData({ tlsGroupLabels: ['X25519'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('X25519');
});

it('should render node metrics', async () => {
const { container } = render(
<ElementPanelMetrics
Expand Down
145 changes: 143 additions & 2 deletions web/src/components/drawer/element/element-panel-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,27 @@ import { FilterIcon, TimesIcon } from '@patternfly/react-icons';
import { BaseEdge, BaseNode } from '@patternfly/react-topology';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Filter, FilterDefinition, Filters } from '../../../model/filters';
import { GraphElementPeer, isElementFiltered, NodeData, toggleElementFilter } from '../../../model/topology';
import {
createFilterValue,
doesIncludeFilter,
Filter,
FilterCompare,
FilterDefinition,
Filters,
type FilterId
} from '../../../model/filters';
import {
EdgeTlsPanelData,
GraphElementPeer,
isElementFiltered,
NodeData,
toggleElementFilter,
toggleQuickFilterValue
} from '../../../model/topology';
import { findFilter } from '../../../utils/filter-definitions';
import { createPeer } from '../../../utils/metrics';
import { tlsLockSeverityForGroupLabel } from '../../../utils/tls-lock-severity';
import { TlsSeverityLockIcon, TlsVersionLockIcon } from '../../icons/tls-lock-icons';
import { ElementFields } from './element-fields';

export interface ElementPanelContentProps {
Expand Down Expand Up @@ -121,6 +139,127 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
[t]
);

const edgeTlsInfo = React.useCallback(
(d: EdgeTlsPanelData | undefined) => {
const hasTlsData = (panel?: EdgeTlsPanelData) =>
Boolean(panel?.tagTlsSecure) ||
Boolean(panel?.tlsTypeLabels?.length) ||
Boolean(panel?.tlsVersionLabels?.length) ||
Boolean(panel?.tlsGroupLabels?.length);
const panel = hasTlsData(d) ? d : undefined;
if (!panel || !hasTlsData(panel)) {
return <></>;
}
const { tagTlsSecure, tlsTypeLabels, tlsVersionLabels, tlsGroupLabels } = panel;
const versionLabels = tlsVersionLabels ?? [];

const renderTlsQuickFilter = (filterId: FilterId, value: string, buttonId: string) => {
const def = findFilter(filterDefinitions, filterId);
if (!def) {
return null;
}
const filterKey = { def, compare: FilterCompare.equal };
const filterValue = createFilterValue(def, value);
const filterValues = [filterValue];
const isFiltered = doesIncludeFilter(filters.list, filterKey, filterValues);
return (
<Button
id={buttonId}
data-test={`quick-filter-${filterId}-${value}`}
variant="plain"
className="overflow-button"
icon={isFiltered ? <TimesIcon /> : <FilterIcon />}
aria-label={
isFiltered ? t('Clear filter for {{value}}', { value }) : t('Apply filter for {{value}}', { value })
}
onClick={() => toggleQuickFilterValue(def, value, isFiltered, filters.list, setFilters)}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
};

return (
<Content id="edge-tls-info" className="record-field-container">
<Content component={ContentVariants.h4}>{t('TLS')}</Content>
{tagTlsSecure ? (
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>
{t('TLS-encrypted traffic was observed on this link for the selected scope and time range.')}
</Content>
</FlexItem>
</Flex>
) : null}
{versionLabels.length > 0 ? (
<>
<Content component={ContentVariants.h4}>{t('TLS versions')}</Content>
<Flex direction={{ default: 'column' }} gap={{ default: 'gapSm' }}>
{versionLabels.map((label, i) => {
const versionFilterBtn = renderTlsQuickFilter('tls_version', label, `edge-tls-version-filter-${i}`);
return (
<Flex key={`${label}-${i}`} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
<TlsVersionLockIcon versionLabel={label} />
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>{label}</Content>
</FlexItem>
{versionFilterBtn ? <FlexItem>{versionFilterBtn}</FlexItem> : null}
</Flex>
);
})}
</Flex>
</>
) : null}
{tlsGroupLabels && tlsGroupLabels.length > 0 ? (
<>
<Content component={ContentVariants.h4}>{t('TLS groups')}</Content>
<Flex direction={{ default: 'column' }} gap={{ default: 'gapSm' }}>
{tlsGroupLabels.map((label, i) => {
const groupFilterBtn = renderTlsQuickFilter('tls_group', label, `edge-tls-group-filter-${i}`);
const groupSeverity = tlsLockSeverityForGroupLabel(label, versionLabels);
return (
<Flex key={`${label}-${i}`} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
<TlsSeverityLockIcon severity={groupSeverity} />
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>{label}</Content>
</FlexItem>
{groupFilterBtn ? <FlexItem>{groupFilterBtn}</FlexItem> : null}
</Flex>
);
})}
</Flex>
</>
) : null}
{tlsTypeLabels && tlsTypeLabels.length > 0 ? (
<>
<Content component={ContentVariants.h4}>{t('TLS message types')}</Content>
<Flex direction={{ default: 'column' }} gap={{ default: 'gapSm' }}>
{tlsTypeLabels.map((label, i) => {
const typeFilterBtn = renderTlsQuickFilter('tls_types', label, `edge-tls-type-filter-${i}`);
return (
<Flex key={`${label}-${i}`} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>{label}</Content>
</FlexItem>
{typeFilterBtn ? <FlexItem>{typeFilterBtn}</FlexItem> : null}
</Flex>
);
})}
</Flex>
</>
) : tagTlsSecure && !tlsTypeLabels?.length ? (
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.small}>
{t('No TLS message type breakdown is available for this aggregation.')}
</Content>
</FlexItem>
</Flex>
) : null}
</Content>
);
},
[filterDefinitions, filters, setFilters, t]
);

if (element instanceof BaseNode && data) {
return (
<>
Expand All @@ -141,6 +280,7 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
// Edge A to B (prefering neutral naming here as there is no assumption about what is source, what is destination
const aData: NodeData = element.getSource().getData();
const bData: NodeData = element.getTarget().getData();
const edgeData = element.getData();
const combinedData = Object.assign({}, aData, bData);
return (
<>
Expand Down Expand Up @@ -189,6 +329,7 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
</AccordionItem>
</div>
</Accordion>
{edgeTlsInfo(edgeData)}
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/drawer/element/element-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#pf-tab-section-details-drawer-tabs,
#pf-tab-section-metrics-drawer-tabs,
#pf-tab-section-dropped-drawer-tabs,
#pf-tab-section-tls-drawer-tabs,
#pf-tab-section-health-drawer-tabs {
overflow-x: hidden !important;
overflow-y: auto !important;
Expand Down
Loading
Loading