Skip to content

Commit

Permalink
Merge pull request #1081 from pangeacyber/cfagan/GEA-14113
Browse files Browse the repository at this point in the history
GEA-14113: Should highlighting audit fpe context
  • Loading branch information
cj-f committed May 30, 2024
2 parents e008978 + 8a1c913 commit f7c7a4f
Show file tree
Hide file tree
Showing 22 changed files with 802 additions and 117 deletions.
18 changes: 18 additions & 0 deletions packages/react-mui-audit-log-viewer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.78] - 2024-05-30

### Fixed

- Typing "Enter" to trigger search now reflects current query in search input on search, instead of out of sync query

## [0.0.77] - 2024-05-17

### Added

- Added searchOnFilterChange boolean prop, to allow the user to control if the AuditLogViewer should trigger a search on filters change. Default: true

## [0.0.75] - 2024-05-15

### Added

- Added fpeOptions prop, to control whether format preserving encryption should be highlighted

## [0.0.74] - 2024-05-02

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions packages/react-mui-audit-log-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ The AuditLogViewer component accepts the following props:
- initialQuery (optional): A string representing the default initial search query.
- onSearch: A function that takes a body of type Audit.SearchRequest and returns a promise resolving to a Audit.SearchResponse. This function is called when the user performs a search. Should make a call to the Audit Service `/search` endpoint proxied through your application server.
- searchOnChange (optional): A optional boolean to prevent the search input to auto search on change. If false will only search when the "Search" button is clicked or if "Enter" is typed while focused on the search input. Default: true
- searchOnFilterChange (optional): A optional boolean to trigger search on component mount. By default searchOnChange triggers a search but if it is disabled then searchOnFilterChange can be enabled
- onPageChange: A function that takes a body of type Audit.ResultRequest and returns a promise resolving to a Audit.ResultResponse. This function is called when the user navigates to a different page of results. Should make a call to the Audit Service `/results` endpoint proxied through your application server.
- verificationOptions (optional): An object containing verification options. Letting you control whether or not to include client side verification check on audit logs.
- onFetchRoot: A function that takes a body of type Audit.RootRequest and returns a promise resolving to a Audit.RootResponse. This function is called when the root data needs to be fetched. Should make a call to the Audit Service `/root` endpoint proxied through your application server.
Expand All @@ -78,6 +79,8 @@ The AuditLogViewer component accepts the following props:
- schema (optional): An object representing the audit schema. With Audit Service custom schema support, you can change the expected Audit schema. This will control what fields are rendered.
- schemaOptions (optional): An object representing options for mutating the audit schema the component uses.
- hiddenFields (optional): A list of field ids to hide/remove from the schema before it is rendered. Will completely hide the field from the table, visibility options, and filtering
- fpeOptions (optional): An object representing options for highlighting and retrieving format preserving encryption (FPE) context for each audit log. An audit event only has FPE context if FPE redaction was used on the log, from a Pangea Redact service integration
- highlightRedaction (optional): A boolean for controlling whether we highlight values in the log which were redacted with FPE

For a deeper dive into the Prop interface check the source code [here](https://github.com/pangeacyber/pangea-javascript/blob/main/packages/react-mui-audit-log-viewer/src/AuditLogViewer.tsx)

Expand Down
29 changes: 29 additions & 0 deletions packages/react-mui-audit-log-viewer/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
collectCoverageFrom: [
"**/*.{js,jsx,ts,tsx}",
"!**/*.d.ts",
"!**/node_modules/**",
"!**/*.stories.tsx",
],
moduleNameMapper: {
/* Handle CSS imports (with CSS modules)
https://jestjs.io/docs/webpack#mocking-css-modules */
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",

// Handle CSS imports (without CSS modules)
"^.+\\.(css|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",

/* Handle image imports
https://jestjs.io/docs/webpack#handling-static-assets */
"^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$":
"<rootDir>/__mocks__/fileMock.js",
},
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
transform: {
"^.+\\.(ts|tsx)$": ["ts-jest"],
},
transformIgnorePatterns: [
"/node_modules/",
"^.+\\.module\\.(css|sass|scss)$",
],
};
11 changes: 7 additions & 4 deletions packages/react-mui-audit-log-viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pangeacyber/react-mui-audit-log-viewer",
"version": "0.0.74",
"version": "0.0.78",
"description": "An extension of material ui data-grid for Pangea audit log records",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand All @@ -11,7 +11,8 @@
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"build": "NODE_ENV=production rollup -c"
"build": "NODE_ENV=production rollup -c",
"test": "jest"
},
"devDependencies": {
"@babel/core": "^7.24.6",
Expand Down Expand Up @@ -55,13 +56,15 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@pangeacyber/react-mui-shared": "0.0.59",
"@pangeacyber/react-mui-shared": "0.0.61",
"@types/jest": "^29.5.12",
"crypto-js": "^4.2.0",
"diff": "^5.2.0",
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"merkle-tools": "^1.4.1",
"react-beautiful-dnd": "^13.1.1"
"react-beautiful-dnd": "^13.1.1",
"ts-jest": "^29.1.2"
},
"peerDependencies": {
"@emotion/react": "^11.10.4",
Expand Down
28 changes: 25 additions & 3 deletions packages/react-mui-audit-log-viewer/src/AuditLogViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface AuditLogViewerProps<Event = Audit.DefaultEvent> {

onSearch: (body: Audit.SearchRequest) => Promise<Audit.SearchResponse>;
searchOnChange?: boolean;
searchOnFilterChange?: boolean;

onPageChange: (body: Audit.ResultRequest) => Promise<Audit.ResultResponse>;
onDownload?: (
Expand All @@ -31,6 +32,11 @@ export interface AuditLogViewerProps<Event = Audit.DefaultEvent> {
ModalChildComponent?: FC;
onCopy?: (message: string, value: string) => void;
};

fpeOptions?: {
highlightRedaction?: boolean;
};

sx?: SxProps;
pageSize?: number;
dataGridProps?: Partial<DataGridProps>;
Expand All @@ -55,6 +61,7 @@ const AuditLogViewerWithProvider = <Event,>({
schemaOptions,
initialQuery,
filters,
fpeOptions,
...props
}: AuditLogViewerProps<Event>): JSX.Element => {
const { schema } = useSchema(config, schemaProp, schemaOptions);
Expand All @@ -70,7 +77,12 @@ const AuditLogViewerWithProvider = <Event,>({

const handleSearch = (body: Audit.SearchRequest): Promise<void> => {
setLoading(true);
return onSearch(body)
return onSearch({
...body,
...(!!fpeOptions?.highlightRedaction && {
return_context: true,
}),
})
.then((response) => {
setLoading(false);
if (!response || response?.events === undefined) {
Expand Down Expand Up @@ -99,7 +111,12 @@ const AuditLogViewerWithProvider = <Event,>({

const handleResults = (body: Audit.ResultRequest): Promise<void> => {
setLoading(true);
return onPageChange(body)
return onPageChange({
...body,
...(!!fpeOptions?.highlightRedaction && {
return_context: true,
}),
})
.then((response) => {
setLoading(false);
if (!response) return;
Expand All @@ -120,7 +137,12 @@ const AuditLogViewerWithProvider = <Event,>({
if (!onDownload) return;

setLoading(true);
return onDownload(body)
return onDownload({
...body,
...(!!fpeOptions?.highlightRedaction && {
return_context: true,
}),
})
.then((response) => {
setLoading(false);
if (response?.dest_url) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import { Typography } from "@mui/material";
import { FC, ReactNode } from "react";
import { Audit } from "../../types";
import {
getFieldMatches,
injectFPEMatchesIntoChanges,
useFpeContext,
} from "../../hooks/fpe";
import { Change } from "../../hooks/diff";
import { ChangesTypography } from "../AuditStringJsonField";

export const StringCell: FC<{ value: ReactNode }> = (params) => {
const { value } = params;
export const StringCell: FC<{
id: string;
field: string;
value: ReactNode;
row: Audit.FlattenedAuditRecord;
}> = (params) => {
const { value, row, field, id } = params;

return <Typography variant="body2">{value}</Typography>;
const context = useFpeContext(row);

let changes: Change[] = [
{
// @ts-ignore
value: `${value}`,
},
];

try {
const matches = getFieldMatches(field, context);
changes = injectFPEMatchesIntoChanges(matches, changes);
} catch {
// Unable to generate changes based on fpe matches
}

return (
<ChangesTypography
value={`${value}`}
changes={changes}
uniqueId={`row-${id}-${field}`}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useMemo } from "react";
import { FC, useCallback, useEffect, useMemo, useRef } from "react";
import find from "lodash/find";

import { Box } from "@mui/material";
Expand Down Expand Up @@ -48,6 +48,7 @@ export interface ViewerProps<Event = Audit.DefaultEvent> {
visibilityModel?: Partial<Record<keyof Event, boolean>>;
filters?: PublicAuditQuery;
searchOnChange?: boolean;
searchOnFilterChange?: boolean;
}

const AuditLogViewerComponent: FC<ViewerProps> = ({
Expand All @@ -60,6 +61,7 @@ const AuditLogViewerComponent: FC<ViewerProps> = ({
dataGridProps = {},
fields,
searchOnChange = true,
searchOnFilterChange = true,
}) => {
const {
visibilityModel,
Expand All @@ -74,7 +76,7 @@ const AuditLogViewerComponent: FC<ViewerProps> = ({
setQueryObj,
setSort,
} = useAuditContext();
const { body } = useAuditBody(limit, maxResults);
const { body, bodyWithoutQuery } = useAuditBody(limit, maxResults);
const pagination = usePagination();
const defaultVisibility = useDefaultVisibility(schema);
const defaultOrder = useDefaultOrder(schema);
Expand All @@ -84,11 +86,19 @@ const AuditLogViewerComponent: FC<ViewerProps> = ({
const filterFields = useAuditFilterFields(schema);
const conditionalOptions = useAuditConditionalOptions(schema);

const bodyRef = useRef(body);
bodyRef.current = body;

const handleSearch = () => {
if (!body) return;
return onSearch(body);
if (!bodyRef.current) return;
return onSearch(bodyRef.current);
};

useEffect(() => {
if (!!bodyWithoutQuery && searchOnFilterChange && !searchOnChange)
handleSearch();
}, [bodyWithoutQuery, searchOnFilterChange, searchOnChange]);

useEffect(() => {
if (!!body && searchOnChange) handleSearch();
}, [body, searchOnChange]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useMemo, useState } from "react";
import isEmpty from "lodash/isEmpty";

import { Stack, Typography, Switch } from "@mui/material";
Expand All @@ -7,6 +7,12 @@ import { Audit } from "../../types";
import { useDiffWords } from "../../hooks/diff";

import StringJsonField from "../AuditStringJsonField";
import {
FPEContext,
getFieldMatches,
injectFPEMatchesIntoChanges,
updateMatchesToHaveRelativeRanges,
} from "../../hooks/fpe";

interface Props {
event: {
Expand All @@ -15,53 +21,71 @@ interface Props {
};
direction: "row" | "column";
uniqueId: string;
context?: FPEContext;
}

const OldNewFields: FC<Props> = ({ event, direction, uniqueId }) => {
const [showDiff, setShowDiff] = useState(true);
const OldNewFields: FC<Props> = ({ event, direction, uniqueId, context }) => {
const changes = useDiffWords(event.old, event.new);

const shouldShowDiffs = showDiff && !isEmpty(changes);
const oldChanges = useMemo(() => {
const matches = getFieldMatches("old", context);
const changes_ = changes.filter((c) => !c.added);

try {
// Adjust field FPE matches to be relative to the entire strinified object
// to work with the used JSONViewer, which does not have support for highlighting
// values based on their JSON path
const adjustedMatches = updateMatchesToHaveRelativeRanges(
event.old ?? "",
matches
);
return injectFPEMatchesIntoChanges(adjustedMatches, changes_);
} catch {
// Unable to adjust field matches to be relative to complete string
}

return changes_;
}, []);

const newChanges = useMemo(() => {
const matches = getFieldMatches("new", context);
const changes_ = changes.filter((c) => !c.removed);

try {
// Adjust field FPE matches to be relative to the entire strinified object
// to work with the used JSONViewer, which does not have support for highlighting
// values based on their JSON path
const adjustedMatches = updateMatchesToHaveRelativeRanges(
event.new ?? "",
matches
);
return injectFPEMatchesIntoChanges(adjustedMatches, changes_);
} catch {
// Unable to adjust field matches to be relative to complete string
}

return changes_;
}, []);

return (
<Stack sx={{ position: "relative" }} spacing={-1}>
{!isEmpty(changes) && false && (
<Stack
direction="row"
spacing={1}
marginLeft="auto"
paddingRight={2}
sx={{
alignItems: "center",
}}
>
<Typography color="textSecondary" variant="body2">
{showDiff ? "Hide differences" : "Show differences"}
</Typography>
<Switch
checked={showDiff}
color="info"
onClick={() => setShowDiff(!showDiff)}
/>
</Stack>
)}
{!!event.old && (
<StringJsonField
title="Old Value"
field="old"
value={event.old}
shouldHighlight={(c) => !!c.removed}
changes={shouldShowDiffs ? changes.filter((c) => !c.added) : []}
shouldHighlight={(c) => !!c.removed || !!c.redacted}
changes={oldChanges}
uniqueId={uniqueId}
/>
)}
{!!event.new && (
<StringJsonField
title="New Value"
field="old"
field="new"
value={event.new}
shouldHighlight={(c) => !!c.added}
changes={shouldShowDiffs ? changes.filter((c) => !c.removed) : []}
shouldHighlight={(c) => !!c.added || !!c.redacted}
changes={newChanges}
uniqueId={uniqueId}
/>
)}
Expand Down
Loading

0 comments on commit f7c7a4f

Please sign in to comment.