= ({
diff --git a/web/src/utils/__tests__/columns.spec.ts b/web/src/utils/__tests__/columns.spec.ts
new file mode 100644
index 000000000..b9a4fe933
--- /dev/null
+++ b/web/src/utils/__tests__/columns.spec.ts
@@ -0,0 +1,100 @@
+import { Record } from '../../api/ipfix';
+import { FullConfigResultSample } from '../../components/__tests-data__/config';
+import { ColumnsId, getDefaultColumns } from '../columns';
+
+describe('Columns', () => {
+ const flow: Record = {
+ fields: {
+ SrcAddr: '10.0.0.1',
+ SrcPort: 42000,
+ DstAddr: '10.0.0.2',
+ DstPort: 8080,
+ SrcK8S_Name: 'client',
+ DstK8S_Name: 'server'
+ },
+ labels: {
+ SrcK8S_Namespace: 'foo',
+ DstK8S_Namespace: 'bar',
+ SrcK8S_Type: 'Pod',
+ DstK8S_Type: 'Service'
+ },
+ key: 0
+ };
+
+ const { columns, fields } = FullConfigResultSample;
+ const defColumns = getDefaultColumns(columns, fields);
+
+ it('should calculate IP+Port value', () => {
+ const col = defColumns.find(c => c.id === ('SrcAddrPort' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual('10.0.0.1:42000');
+ });
+
+ it('should calculate src+dst IP+Port values', () => {
+ const col = defColumns.find(c => c.id === ('AddrPort' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual(['10.0.0.1:42000', '10.0.0.2:8080']);
+ });
+
+ it('should calculate k8s name values', () => {
+ const col = defColumns.find(c => c.id === ('SrcK8S_Name' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual({ kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: false });
+ });
+
+ it('should calculate k8s owner name when empty', () => {
+ const col = defColumns.find(c => c.id === ('DstK8S_OwnerName' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toBeUndefined();
+ });
+
+ it('should calculate k8s namespace values', () => {
+ const col = defColumns.find(c => c.id === ('SrcK8S_Namespace' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual({ kind: 'Namespace', name: 'foo', showNamespace: false });
+ });
+
+ it('should calculate k8s object values', () => {
+ const col = defColumns.find(c => c.id === ('SrcK8S_Object' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual({ kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: true });
+ });
+
+ it('should fallback on IP', () => {
+ const withoutName: Record = { ...flow, fields: { ...flow.fields, SrcK8S_Name: undefined } };
+ const col = defColumns.find(c => c.id === ('SrcK8S_Object' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(withoutName);
+ expect(value).toEqual('10.0.0.1:42000');
+ });
+
+ it('should calculate src+dst K8S types', () => {
+ const col = defColumns.find(c => c.id === ('K8S_Type' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual(['Pod', 'Service']);
+ });
+
+ it('should calculate src+dst K8S objects', () => {
+ const col = defColumns.find(c => c.id === ('K8S_Object' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual([
+ { kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: true },
+ { kind: 'Service', name: 'server', namespace: 'bar', showNamespace: true }
+ ]);
+ });
+
+ it('should calculate src+dst K8S owner objects when empty', () => {
+ const col = defColumns.find(c => c.id === ('K8S_OwnerObject' as ColumnsId));
+ expect(col).toBeDefined();
+ const value = col?.value!(flow);
+ expect(value).toEqual([undefined, undefined]);
+ });
+});
diff --git a/web/src/utils/column-parser.ts b/web/src/utils/column-parser.ts
new file mode 100644
index 000000000..fb36df79c
--- /dev/null
+++ b/web/src/utils/column-parser.ts
@@ -0,0 +1,161 @@
+import { getRecordValue, Record } from '../api/ipfix';
+import { Column, ColumnConfigDef, ColumnsId, ColValue } from './columns';
+import { FieldConfig, FieldType } from './fields';
+
+const getColumnOrRecordValue = (
+ columns: Column[],
+ record: Record,
+ arg: string,
+ defaultValue: string | number
+): ColValue => {
+ if (arg.startsWith(`'`) && arg.endsWith(`'`)) {
+ // literal
+ return arg.substring(1, arg.length - 1);
+ } else if (arg.startsWith('column.')) {
+ const colId = arg.replace('column.', '');
+ const found = columns.find(c => c.id === colId);
+ if (found && found.value) {
+ return found.value(record);
+ }
+ return defaultValue;
+ }
+ return getRecordValue(record, arg, defaultValue);
+};
+
+const funcs: { [name: string]: (columns: Column[], record: Record, args: string[]) => ColValue } = {
+ concat: (columns: Column[], record: Record, args: string[]): ColValue => {
+ if (args.length < 2) {
+ console.error('getDefaultColumns - invalid parameters for concat calculated value', args);
+ return '';
+ }
+ return args.map(a => getColumnOrRecordValue(columns, record, a, '')).join('');
+ },
+ kubeObject: (columns: Column[], record: Record, args: string[]): ColValue => {
+ if (args.length !== 4) {
+ console.error('getDefaultColumns - invalid parameters for kubeObject calculated value', args);
+ return '';
+ }
+ const kind = String(getColumnOrRecordValue(columns, record, args[0], ''));
+ const namespace = String(getColumnOrRecordValue(columns, record, args[1], ''));
+ const name = String(getColumnOrRecordValue(columns, record, args[2], ''));
+ if (!name || !kind) {
+ return undefined;
+ }
+ return {
+ kind: kind,
+ name: name,
+ namespace: namespace || undefined, // convert empty string to undefined
+ showNamespace: args[3] === '1'
+ };
+ },
+ substract: (columns: Column[], record: Record, args: string[]): ColValue => {
+ if (args.length !== 2) {
+ console.error('getDefaultColumns - invalid parameters for substract calculated value', args);
+ return '';
+ }
+ return (
+ (getColumnOrRecordValue(columns, record, args[0], 0) as number) -
+ (getColumnOrRecordValue(columns, record, args[1], 0) as number)
+ );
+ },
+ multiply: (columns: Column[], record: Record, args: string[]): ColValue => {
+ if (args.length !== 2) {
+ console.error('getDefaultColumns - invalid parameters for multiply calculated value', args);
+ return '';
+ }
+ return (getColumnOrRecordValue(columns, record, args[0], 0) as number) * Number(args[1]);
+ }
+};
+
+const parseORs = (columns: Column[], calculatedValue: string): ((record: Record) => ColValue)[] => {
+ // parseORs returns a closure [(Record) => ColValue] to pre-process as much as possible
+ const ors = calculatedValue.split(' or ');
+ return ors.map(or => {
+ for (const name in funcs) {
+ if (or.startsWith(name + '(')) {
+ const regex = new RegExp(name + '|\\(|\\)', 'g');
+ const repl = or.replaceAll(regex, '');
+ const args = repl.split(',');
+ return (record: Record) => funcs[name](columns, record, args);
+ }
+ }
+ return () => undefined;
+ });
+};
+
+const forceType = (id: ColumnsId, value: ColValue, type?: FieldType): ColValue => {
+ if (!type) {
+ console.error('Column ' + id + " doesn't specify type");
+ }
+ // check if value type match and convert it if needed
+ if (value && value !== '' && typeof value !== type && !Array.isArray(value)) {
+ switch (type) {
+ case 'number':
+ return Number(value);
+ case 'string':
+ return String(value);
+ default:
+ throw new Error('forceType error: type ' + type + ' is not managed');
+ }
+ } else {
+ // else return value directly
+ return value;
+ }
+};
+
+export type ValueFunc = (record: Record) => ColValue;
+
+export const fromFieldFunc = (
+ def: ColumnConfigDef,
+ fields: FieldConfig[] | undefined,
+ field: FieldConfig | undefined
+): ValueFunc | undefined => {
+ if (fields) {
+ return (r: Record) => {
+ const result: ColValue[] = fields.map(fc => {
+ const value = getRecordValue(r, fc.name, undefined);
+ return forceType(def.id as ColumnsId, value, fc.type);
+ });
+ return result.flatMap(r => r) as ColValue;
+ };
+ } else if (field) {
+ return (r: Record) => {
+ const value = getRecordValue(r, field!.name, '');
+ return forceType(def.id as ColumnsId, value, field!.type);
+ };
+ }
+ return undefined;
+};
+
+export const computeValueFunc = (
+ def: ColumnConfigDef,
+ columns: Column[],
+ fields: FieldConfig[] | undefined,
+ field: FieldConfig | undefined
+): ValueFunc | undefined => {
+ if (def.calculated) {
+ if (def.calculated.startsWith('[') && def.calculated.endsWith(']')) {
+ const values = def.calculated.replaceAll(/\[|\]/g, '').split(',');
+ return (r: Record) => {
+ const result = values.map(v => getColumnOrRecordValue(columns, r, v, ''));
+ return result.flatMap(r => r) as ColValue;
+ };
+ }
+ const orFuncs = parseORs(columns, def.calculated!);
+ return (r: Record) => {
+ for (const orFunc of orFuncs) {
+ const result = orFunc(r);
+ if (result) {
+ return result;
+ }
+ }
+ return undefined;
+ };
+ }
+
+ const fromField = fromFieldFunc(def, fields, field);
+ if (fromField === undefined) {
+ console.warn('column.value called on ' + def.id + ' but not configured');
+ }
+ return fromField;
+};
diff --git a/web/src/utils/columns.ts b/web/src/utils/columns.ts
index 2e568a1ed..6f30f1004 100644
--- a/web/src/utils/columns.ts
+++ b/web/src/utils/columns.ts
@@ -1,9 +1,10 @@
import _ from 'lodash';
-import { getRecordValue, Record } from '../api/ipfix';
+import { Record } from '../api/ipfix';
import { Feature } from '../model/config';
import { FilterId } from '../model/filters';
import { compareNumbers, compareStrings } from './base-compare';
-import { FieldConfig, FieldType } from './fields';
+import { computeValueFunc, fromFieldFunc } from './column-parser';
+import { FieldConfig } from './fields';
import { compareIPs } from './ip';
import { comparePorts } from './port';
import { compareProtocols } from './protocol';
@@ -36,8 +37,6 @@ export enum ColumnsId {
srcport = 'SrcPort',
dstport = 'DstPort',
addrport = 'AddrPort',
- srcaddrport = 'SrcAddrPort',
- dstaddrport = 'DstAddrPort',
proto = 'Proto',
icmptype = 'IcmpType',
icmpcode = 'IcmpCode',
@@ -98,6 +97,17 @@ export interface ColumnConfigDef {
feature?: Feature;
}
+export interface KubeObj {
+ name: string;
+ kind: string;
+ namespace?: string;
+ showNamespace: boolean;
+}
+export type ColValue = string | number | KubeObj | string[] | number[] | KubeObj[] | undefined;
+export const isKubeObj = (v: ColValue): v is KubeObj => {
+ return (v as KubeObj)?.kind !== undefined;
+};
+
export interface Column {
id: ColumnsId;
group?: string;
@@ -108,7 +118,8 @@ export interface Column {
quickFilter?: FilterId;
isSelected: boolean;
isCommon?: boolean;
- value: (flow: Record) => string | number | string[] | number[];
+ value?: (flow: Record) => ColValue;
+ fieldValue?: (flow: Record) => ColValue;
sort(a: Record, b: Record, col: Column): number;
// width in "em"
width: number;
@@ -190,115 +201,9 @@ export const getShortColumnName = (col?: Column): string => {
return '';
};
-export const getSrcOrDstValue = (v1?: string | number, v2?: string | number): string[] | number[] => {
- if (v1 && !Number.isNaN(v1) && v2 && !Number.isNaN(v2)) {
- return [v1 as number, v2 as number];
- } else if (v1 || v2) {
- return [v1 ? (v1 as string) : '', v2 ? (v2 as string) : ''];
- } else {
- return [];
- }
-};
-
-/* concatenate kind / namespace / pod or ip / port for display
- * if kubernetes objects Kind Namespace & Pod field are not resolved, will fallback on
- * ip:port or ip only if port is not provided
- */
-export const getConcatenatedValue = (
- ip: string,
- port: number,
- kind?: string,
- namespace?: string,
- pod?: string
-): string => {
- if (kind && namespace && pod) {
- return `${kind}.${namespace}.${pod}`;
- }
- if (!Number.isNaN(port)) {
- return `${ip}:${String(port)}`;
- }
- return ip;
-};
-
export const getDefaultColumns = (columnDefs: ColumnConfigDef[], fieldConfigs: FieldConfig[]): Column[] => {
const columns: Column[] = [];
- function getColumnOrRecordValue(record: Record, arg: string, defaultValue: string | number) {
- if (arg.startsWith('column.')) {
- const colId = arg.replace('column.', '');
- const found = columns.find(c => c.id === colId);
- if (found) {
- return found.value(record);
- }
- return defaultValue;
- }
- return getRecordValue(record, arg, defaultValue);
- }
-
- function calculatedValue(record: Record, calculatedValue: string) {
- if (calculatedValue.startsWith('getSrcOrDstValue')) {
- const args = calculatedValue.replaceAll(/getSrcOrDstValue|\(|\)/g, '').split(',');
- if (args.length !== 2) {
- console.error('getDefaultColumns - invalid parameters for getSrcOrDstValue calculated value', calculatedValue);
- return '';
- }
- return getSrcOrDstValue(...args.flatMap(f => getColumnOrRecordValue(record, f, '')));
- } else if (calculatedValue.startsWith('getConcatenatedValue')) {
- const args = calculatedValue.replaceAll(/getConcatenatedValue|\(|\)/g, '').split(',');
- if (args.length < 2) {
- console.error(
- 'getDefaultColumns - invalid parameters for getConcatenatedValue calculated value',
- calculatedValue
- );
- return '';
- }
- return getConcatenatedValue(
- getColumnOrRecordValue(record, args[0], '') as string,
- getColumnOrRecordValue(record, args[1], NaN) as number,
- args.length > 2 ? (getColumnOrRecordValue(record, args[2], '') as string) : undefined,
- args.length > 3 ? (getColumnOrRecordValue(record, args[3], '') as string) : undefined
- );
- } else if (calculatedValue.startsWith('substract')) {
- const args = calculatedValue.replaceAll(/substract|\(|\)/g, '').split(',');
- if (args.length < 2) {
- console.error('getDefaultColumns - invalid parameters for substract calculated value', calculatedValue);
- return '';
- }
- return (
- (getColumnOrRecordValue(record, args[0], 0) as number) - (getColumnOrRecordValue(record, args[1], 0) as number)
- );
- } else if (calculatedValue.startsWith('multiply')) {
- const args = calculatedValue.replaceAll(/multiply|\(|\)/g, '').split(',');
- if (args.length < 2) {
- console.error('getDefaultColumns - invalid parameters for multiply calculated value', calculatedValue);
- return '';
- }
- return (getColumnOrRecordValue(record, args[0], 0) as number) * Number(args[1]);
- }
- return '';
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function forceType(id: ColumnsId, value: any, type?: FieldType) {
- if (!type) {
- console.error('Column ' + id + " doesn't specify type");
- }
- // check if value type match and convert it if needed
- if (value && value !== '' && typeof value !== type && !Array.isArray(value)) {
- switch (type) {
- case 'number':
- return Number(value);
- case 'string':
- return String(value);
- default:
- throw new Error('forceType error: type ' + type + ' is not managed');
- }
- } else {
- // else return value directly
- return value;
- }
- }
-
// add a column for each definition
columnDefs.forEach(d => {
const id = d.id as ColumnsId;
@@ -322,65 +227,29 @@ export const getDefaultColumns = (columnDefs: ColumnConfigDef[], fieldConfigs: F
docURL: !_.isEmpty(d.docURL) ? d.docURL : undefined,
quickFilter: !_.isEmpty(d.filter) ? (d.filter as FilterId) : undefined,
isSelected: d.default === true,
- isCommon: !_.isEmpty(d.calculated),
- value: (r: Record) => {
- if (!_.isEmpty(d.calculated)) {
- if (d.calculated!.startsWith('[') && d.calculated!.endsWith(']')) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const result: any = [];
- if (d.calculated?.includes('column')) {
- // consider all items as columns or fields
- const values = d.calculated!.replaceAll(/\[|\]/g, '').split(',');
- values.forEach(v => {
- result.push(getColumnOrRecordValue(r, v, ''));
- });
- } else {
- // consider all items as functions
- const values = d.calculated!.replaceAll(/\[|\]/g, '').split('),');
- values.forEach(v => {
- result.push(calculatedValue(r, `${v})`));
- });
- }
- return result;
- } else {
- return calculatedValue(r, d.calculated!);
- }
- } else if (fields) {
- const arr: (string | number | undefined)[] = [];
- fields.forEach(fc => {
- const value = getRecordValue(r, fc.name, undefined);
- arr.push(forceType(id, value, fc.type));
- });
- return arr;
- } else if (field) {
- const value = getRecordValue(r, field!.name, '');
- return forceType(id, value, field!.type);
- } else {
- console.warn('column.value called on ' + id + ' but not configured');
- return null;
- }
- },
+ isCommon: d.calculated !== undefined && d.calculated.startsWith('['),
+ value: computeValueFunc(d, columns, fields, field),
+ fieldValue: fromFieldFunc(d, fields, field),
sort: (a: Record, b: Record, col: Column) => {
- if (d.calculated) {
+ if (!col.fieldValue) {
return -1;
- } else {
- const valA = col.value(a);
- const valB = col.value(b);
- if (typeof valA === 'number' && typeof valB === 'number') {
- if (col.id.includes('Port')) {
- return comparePorts(valA, valB);
- } else if (col.id.includes('Proto')) {
- return compareProtocols(valA, valB);
- }
- return compareNumbers(valA, valB);
- } else if (typeof valA === 'string' && typeof valB === 'string') {
- if (col.id.includes('IP')) {
- return compareIPs(valA, valB);
- }
- return compareStrings(valA, valB);
+ }
+ const valA = col.fieldValue(a);
+ const valB = col.fieldValue(b);
+ if (typeof valA === 'number' && typeof valB === 'number') {
+ if (col.id.includes('Port')) {
+ return comparePorts(valA, valB);
+ } else if (col.id.includes('Proto')) {
+ return compareProtocols(valA, valB);
+ }
+ return compareNumbers(valA, valB);
+ } else if (typeof valA === 'string' && typeof valB === 'string') {
+ if (col.id.includes('IP')) {
+ return compareIPs(valA, valB);
}
- return 0;
+ return compareStrings(valA, valB);
}
+ return 0;
},
width: d.width || 15,
feature: d.feature
|