From a1e140d2b7758d277fcab459f424c315d817648a Mon Sep 17 00:00:00 2001 From: BigJohn-dev Date: Fri, 24 Jul 2026 10:11:00 +0100 Subject: [PATCH] Add unit tests for security analysis utilities - Implement tests for various detection functions including rate anomalies, off-hours activity, actor velocity anomalies, multi-actor bursts, escalation chains, repeated failures, session drift, and category concentration. - Create test factories for generating mock entries to simulate different scenarios. - Validate scoring and risk computation functions with comprehensive test cases. - Ensure integration tests cover the full analysis pipeline with realistic audit data. --- node_modules/.bin/baseline-browser-mapping | 4 +- node_modules/.bin/browserslist | 8 +- node_modules/.bin/esbuild | 11 +- node_modules/.bin/jsesc | 4 +- node_modules/.bin/json5 | 4 +- node_modules/.bin/loose-envify | 4 +- node_modules/.bin/nanoid | 4 +- node_modules/.bin/parser | 4 +- node_modules/.bin/rollup | 8 +- node_modules/.bin/semver | 4 +- node_modules/.bin/sha.js | 4 +- node_modules/.bin/update-browserslist-db | 4 +- node_modules/.bin/vite | 22 +- package.json | 6 +- pnpm-lock.yaml | 296 ++++--- .../dashboard/SecurityAuditPanel.jsx | 608 +++++++++++++ src/hooks/useSecurityAnalysis.js | 100 +++ src/utils/securityAnalysis.js | 812 ++++++++++++++++++ tests/unit/utils/securityAnalysis.test.js | 758 ++++++++++++++++ 19 files changed, 2526 insertions(+), 139 deletions(-) create mode 100644 src/components/dashboard/SecurityAuditPanel.jsx create mode 100644 src/hooks/useSecurityAnalysis.js create mode 100644 src/utils/securityAnalysis.js create mode 100644 tests/unit/utils/securityAnalysis.test.js diff --git a/node_modules/.bin/baseline-browser-mapping b/node_modules/.bin/baseline-browser-mapping index bd699d95..66dec50e 100755 --- a/node_modules/.bin/baseline-browser-mapping +++ b/node_modules/.bin/baseline-browser-mapping @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@" diff --git a/node_modules/.bin/browserslist b/node_modules/.bin/browserslist index f04a911a..680fc65f 100755 --- a/node_modules/.bin/browserslist +++ b/node_modules/.bin/browserslist @@ -10,12 +10,12 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/browserslist/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules/browserslist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/browserslist/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules/browserslist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then - exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@" + exec "$basedir/node" "$basedir/../.pnpm/browserslist@4.28.2/node_modules/browserslist/cli.js" "$@" else - exec node "$basedir/../browserslist/cli.js" "$@" + exec node "$basedir/../.pnpm/browserslist@4.28.2/node_modules/browserslist/cli.js" "$@" fi diff --git a/node_modules/.bin/esbuild b/node_modules/.bin/esbuild index 182e5317..699250af 100755 --- a/node_modules/.bin/esbuild +++ b/node_modules/.bin/esbuild @@ -10,9 +10,12 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/esbuild/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/esbuild/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/esbuild" "$@" +else + exec node "$basedir/../.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/esbuild" "$@" fi -"$basedir/../esbuild/bin/esbuild" "$@" -exit $? diff --git a/node_modules/.bin/jsesc b/node_modules/.bin/jsesc index e1507058..fd90df35 100755 --- a/node_modules/.bin/jsesc +++ b/node_modules/.bin/jsesc @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/jsesc/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/jsesc/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@" diff --git a/node_modules/.bin/json5 b/node_modules/.bin/json5 index 31f748b2..e9618bb0 100755 --- a/node_modules/.bin/json5 +++ b/node_modules/.bin/json5 @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/json5/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/lib/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/json5/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/lib/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@" diff --git a/node_modules/.bin/loose-envify b/node_modules/.bin/loose-envify index c8bed453..feeb9083 100755 --- a/node_modules/.bin/loose-envify +++ b/node_modules/.bin/loose-envify @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@" diff --git a/node_modules/.bin/nanoid b/node_modules/.bin/nanoid index 7464908c..aef8f1dc 100755 --- a/node_modules/.bin/nanoid +++ b/node_modules/.bin/nanoid @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/nanoid/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/nanoid/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@" diff --git a/node_modules/.bin/parser b/node_modules/.bin/parser index 2e46fdf1..a470ae69 100755 --- a/node_modules/.bin/parser +++ b/node_modules/.bin/parser @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@" diff --git a/node_modules/.bin/rollup b/node_modules/.bin/rollup index 2985a6b1..b6c19b67 100755 --- a/node_modules/.bin/rollup +++ b/node_modules/.bin/rollup @@ -10,12 +10,12 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/rollup/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/rollup/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then - exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@" + exec "$basedir/node" "$basedir/../.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/rollup" "$@" else - exec node "$basedir/../rollup/dist/bin/rollup" "$@" + exec node "$basedir/../.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/rollup" "$@" fi diff --git a/node_modules/.bin/semver b/node_modules/.bin/semver index 334837a1..61306a75 100755 --- a/node_modules/.bin/semver +++ b/node_modules/.bin/semver @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/semver/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/semver/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@" diff --git a/node_modules/.bin/sha.js b/node_modules/.bin/sha.js index 37da978c..1c271f80 100755 --- a/node_modules/.bin/sha.js +++ b/node_modules/.bin/sha.js @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/sha.js/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/sha.js/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/sha.js/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/sha.js/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../sha.js/bin.js" "$@" diff --git a/node_modules/.bin/update-browserslist-db b/node_modules/.bin/update-browserslist-db index 89fcf8da..d16ee75a 100755 --- a/node_modules/.bin/update-browserslist-db +++ b/node_modules/.bin/update-browserslist-db @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@" diff --git a/node_modules/.bin/vite b/node_modules/.bin/vite index 6d1e3bea..1bf1adc1 120000 --- a/node_modules/.bin/vite +++ b/node_modules/.bin/vite @@ -1 +1,21 @@ -../vite/bin/vite.js \ No newline at end of file +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/vite.js" "$@" +else + exec node "$basedir/../.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/vite.js" "$@" +fi diff --git a/package.json b/package.json index 2fd5499e..8675470d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "@playwright/test": "^1.59.1", + "@storybook/react-vite": "^8.5.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -51,10 +52,9 @@ "jsdom": "^29.1.1", "msw": "^2.14.3", "prettier": "^3.3.3", + "storybook": "^8.5.0", "typescript": "^6.0.3", "vite": "^5.4.0", - "vitest": "^4.1.5", - "@storybook/react-vite": "^8.5.0", - "storybook": "^8.5.0" + "vitest": "^2.1.9" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21475c4e..eb53477e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,10 +83,10 @@ importers: version: 4.7.0(vite@5.4.21(@types/node@25.6.0)) '@vitest/coverage-v8': specifier: ^4.1.5 - version: 4.1.5(vitest@4.1.5) + version: 4.1.5(vitest@2.1.9) '@vitest/ui': specifier: ^4.1.5 - version: 4.1.5(vitest@4.1.5) + version: 4.1.5(vitest@2.1.9) jsdom: specifier: ^29.1.1 version: 29.1.1 @@ -106,8 +106,8 @@ importers: specifier: ^5.4.0 version: 5.4.21(@types/node@25.6.0) vitest: - specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)) + specifier: ^2.1.9 + version: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3)) packages: @@ -1202,66 +1202,79 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -1296,9 +1309,6 @@ packages: '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@stellar/js-xdr@3.1.2': resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} @@ -1429,9 +1439,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1459,9 +1466,6 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -1527,37 +1531,43 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@4.1.5': - resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@vitest/mocker@4.1.5': - resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^5.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.5': resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.5': - resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@vitest/snapshot@4.1.5': - resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/spy@4.1.5': - resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} '@vitest/ui@4.1.5': resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==} peerDependencies: vitest: 4.1.5 + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} @@ -1691,6 +1701,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1706,14 +1720,18 @@ packages: caniuse-lite@1.0.30001791: resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1831,6 +1849,10 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1892,8 +1914,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -2245,6 +2267,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2387,9 +2412,16 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2663,6 +2695,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -2727,18 +2762,29 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.1: - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} - engines: {node: '>=18'} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.30: resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} @@ -2846,6 +2892,11 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2877,39 +2928,23 @@ packages: terser: optional: true - vitest@4.1.5: - resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.5 - '@vitest/browser-preview': 4.1.5 - '@vitest/browser-webdriverio': 4.1.5 - '@vitest/coverage-istanbul': 4.1.5 - '@vitest/coverage-v8': 4.1.5 - '@vitest/ui': 4.1.5 + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@opentelemetry/api': - optional: true '@types/node': optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': + '@vitest/browser': optional: true '@vitest/ui': optional: true @@ -4168,8 +4203,6 @@ snapshots: '@sinclair/typebox@0.27.10': {} - '@standard-schema/spec@1.1.0': {} - '@stellar/js-xdr@3.1.2': {} '@stellar/stellar-base@12.1.1': @@ -4347,11 +4380,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -4376,8 +4404,6 @@ snapshots: '@types/d3-timer@3.0.2': {} - '@types/deep-eql@4.0.2': {} - '@types/doctrine@0.0.9': {} '@types/estree@1.0.8': {} @@ -4440,7 +4466,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': + '@vitest/coverage-v8@4.1.5(vitest@2.1.9)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.5 @@ -4452,45 +4478,48 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)) + vitest: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3)) - '@vitest/expect@4.1.5': + '@vitest/expect@2.1.9': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - chai: 6.2.2 - tinyrainbow: 3.1.0 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 - '@vitest/mocker@4.1.5(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))': + '@vitest/mocker@2.1.9(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))': dependencies: - '@vitest/spy': 4.1.5 + '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.3(@types/node@25.6.0)(typescript@6.0.3) vite: 5.4.21(@types/node@25.6.0) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.5': + '@vitest/runner@2.1.9': dependencies: - '@vitest/utils': 4.1.5 - pathe: 2.0.3 + '@vitest/utils': 2.1.9 + pathe: 1.1.2 - '@vitest/snapshot@4.1.5': + '@vitest/snapshot@2.1.9': dependencies: - '@vitest/pretty-format': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/pretty-format': 2.1.9 magic-string: 0.30.21 - pathe: 2.0.3 + pathe: 1.1.2 - '@vitest/spy@4.1.5': {} + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 - '@vitest/ui@4.1.5(vitest@4.1.5)': + '@vitest/ui@4.1.5(vitest@2.1.9)': dependencies: '@vitest/utils': 4.1.5 fflate: 0.8.2 @@ -4499,7 +4528,13 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)) + vitest: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3)) + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 '@vitest/utils@4.1.5': dependencies: @@ -4632,6 +4667,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4651,13 +4688,21 @@ snapshots: caniuse-lite@1.0.30001791: {} - chai@6.2.2: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + ci-info@3.9.0: {} cli-width@4.1.0: {} @@ -4758,6 +4803,8 @@ snapshots: decimal.js@10.6.0: {} + deep-eql@5.0.2: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -4805,7 +4852,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@1.7.0: {} es-object-atoms@1.1.1: dependencies: @@ -5204,6 +5251,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@11.3.6: {} @@ -5335,8 +5384,12 @@ snapshots: path-to-regexp@6.3.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5641,6 +5694,8 @@ snapshots: statuses@2.0.2: {} + std-env@3.10.0: {} + std-env@4.1.0: {} storybook@8.6.18(prettier@3.8.3): @@ -5697,15 +5752,21 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.1.1: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + tinyrainbow@3.1.0: {} + tinyspy@3.0.2: {} + tldts-core@7.0.30: {} tldts@7.0.30: @@ -5817,6 +5878,24 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@2.1.9(@types/node@25.6.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@25.6.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite@5.4.21(@types/node@25.6.0): dependencies: esbuild: 0.21.5 @@ -5826,35 +5905,42 @@ snapshots: '@types/node': 25.6.0 fsevents: 2.3.3 - vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)): + vitest@2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3)): dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 + pathe: 1.1.2 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 vite: 5.4.21(@types/node@25.6.0) + vite-node: 2.1.9(@types/node@25.6.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) - '@vitest/ui': 4.1.5(vitest@4.1.5) + '@vitest/ui': 4.1.5(vitest@2.1.9) jsdom: 29.1.1 transitivePeerDependencies: + - less + - lightningcss - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser void-elements@3.1.0: {} diff --git a/src/components/dashboard/SecurityAuditPanel.jsx b/src/components/dashboard/SecurityAuditPanel.jsx new file mode 100644 index 00000000..8b25a975 --- /dev/null +++ b/src/components/dashboard/SecurityAuditPanel.jsx @@ -0,0 +1,608 @@ +import React, { useMemo, useState } from 'react'; +import { + Shield, + AlertTriangle, + AlertOctagon, + CheckCircle, + RefreshCw, + Download, + Filter, + Activity, + Clock, + User, + ChevronDown, + ChevronRight, + TrendingUp, + FileText, +} from 'lucide-react'; +import Card from './Card.tsx'; +import { useSecurityAnalysis } from '../../hooks/useSecurityAnalysis.js'; +import { getAuditEntries, exportAuditJson, exportAuditCsv } from '../../utils/audit.js'; + +const SEVERITY_COLORS = { + info: 'var(--cyan)', + low: 'var(--text-secondary)', + medium: 'var(--yellow)', + high: 'var(--orange)', + critical: 'var(--red)', +}; + +const GRADE_COLORS = { + A: 'var(--green)', + B: 'var(--cyan)', + C: 'var(--yellow)', + D: 'var(--orange)', + F: 'var(--red)', +}; + +const SEVERITY_ICONS = { + info: CheckCircle, + low: Activity, + medium: AlertTriangle, + high: AlertOctagon, + critical: AlertOctagon, +}; + +function RiskGauge({ score, grade }) { + const color = GRADE_COLORS[grade] || 'var(--text-muted)'; + const circumference = 2 * Math.PI * 40; + const offset = circumference - (score / 100) * circumference; + + return ( +
+ + + + + {grade} + + + {score}/100 + + +
+ ); +} + +function FindingRow({ finding, expanded, onToggle }) { + const Icon = SEVERITY_ICONS[finding.severity] || AlertTriangle; + const color = SEVERITY_COLORS[finding.severity] || 'var(--text-muted)'; + + return ( +
+
+ + + {finding.message} + + + {finding.severity} + + {expanded ? ( + + ) : ( + + )} +
+ {expanded && ( +
+
+ Type: {finding.type.replace(/_/g, ' ')} +
+ {finding.actor && ( +
+ Actor: {finding.actor} +
+ )} + {finding.timestamp && ( +
+ + {new Date(finding.timestamp).toLocaleString()} +
+ )} + {finding.count != null && ( +
+ Count: {finding.count} +
+ )} + {finding.expectedMax != null && ( +
+ Expected max: {finding.expectedMax} +
+ )} +
+ )} +
+ ); +} + +function StatBadge({ label, value, color }) { + return ( +
+ + {label} + + {value} + +
+ ); +} + +export default function SecurityAuditPanel() { + const [severityFilter, setSeverityFilter] = useState(''); + const [categoryFilter, setCategoryFilter] = useState(''); + const [expandedFindings, setExpandedFindings] = useState(new Set()); + const [showFilters, setShowFilters] = useState(false); + + const filters = useMemo(() => ({ + ...(severityFilter && { severity: severityFilter }), + ...(categoryFilter && { category: categoryFilter }), + }), [severityFilter, categoryFilter]); + + const { report, findings, riskScore, isAnalyzing, refresh } = useSecurityAnalysis({ + filters, + analysisIntervalMs: 5000, + }); + + const toggleFinding = (index) => { + setExpandedFindings((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + const handleExport = (format) => { + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const entries = getAuditEntries({ ...filters, limit: 1000 }); + const content = format === 'json' ? exportAuditJson(entries) : exportAuditCsv(entries); + const mime = format === 'json' ? 'application/json' : 'text/csv'; + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `security-report-${stamp}.${format}`; + a.click(); + URL.revokeObjectURL(url); + }; + + const bySeveritySummary = report.summary?.bySeverity || {}; + const totalFindings = report.summary?.totalFindings || 0; + + return ( +
+ {/* Header */} +
+
+ +
+
+ Security Analysis +
+
+ AI-powered audit trail anomaly detection +
+
+
+
+ + + +
+
+ + {/* Filters */} + {showFilters && ( +
+
+ + +
+
+ + +
+
+ )} + + {/* Risk Score + Summary Stats */} +
+ 50}> +
+ +
+
+ + +
+
+ + 0 ? 'var(--orange)' : 'var(--green)'} /> + + + + +
+ + {report.timeRange && ( +
+ + Analyzing events from {new Date(report.timeRange.from).toLocaleString()} to{' '} + {new Date(report.timeRange.to).toLocaleString()} +
+ )} + + {/* Breakdown bar */} + {totalFindings > 0 && ( +
+
+ {['critical', 'high', 'medium', 'low', 'info'].map((sev) => { + const count = bySeveritySummary[sev] || 0; + if (!count) return null; + const pct = (count / totalFindings) * 100; + return ( +
+ ); + })} +
+
+ )} +
+ +
+ + {/* Findings List */} + + + +
+ } + > +
+ {findings.length === 0 ? ( +
+ + No security findings detected. All audit events appear normal. +
+ ) : ( +
+ {findings.map((finding, i) => ( + toggleFinding(i)} + /> + ))} +
+ )} +
+ + + {/* Recommendations */} + {report.recommendations.length > 0 && ( + +
+ {report.recommendations.map((rec, i) => ( +
+ + + {rec} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/src/hooks/useSecurityAnalysis.js b/src/hooks/useSecurityAnalysis.js new file mode 100644 index 00000000..7fabc1d3 --- /dev/null +++ b/src/hooks/useSecurityAnalysis.js @@ -0,0 +1,100 @@ +/** + * useSecurityAnalysis Hook + * + * Integrates the security analysis engine with the audit trail system. + * Provides real-time analysis that updates as new audit entries arrive. + * + * @returns {{ + * report: object, + * findings: object[], + * riskScore: object, + * isAnalyzing: boolean, + * refresh: () => void, + * filterEntries: (opts) => object[], + * }} + */ + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { getAuditEntries, subscribeAudit } from '../utils/audit.js'; +import { + generateReport, + computeAggregateRisk, + DEFAULT_CONFIG, + createStreamingEngine, +} from '../utils/securityAnalysis.js'; + +/** + * @param {object} [opts] + * @param {object} [opts.filters] - Audit entry filters (category, severity, actor, etc.) + * @param {number} [opts.analysisIntervalMs=5000] - How often to re-analyze + * @param {object} [opts.config] - Custom analysis config overrides + */ +export function useSecurityAnalysis(opts = {}) { + const { filters = {}, analysisIntervalMs = 5000, config: configOverrides = {} } = opts; + + const config = useMemo( + () => ({ ...DEFAULT_CONFIG, ...configOverrides }), + [JSON.stringify(configOverrides)], // eslint-disable-line react-hooks/exhaustive-deps + ); + + const engineRef = useRef(createStreamingEngine(config)); + const [report, setReport] = useState(() => generateReport([], config)); + const [isAnalyzing, setIsAnalyzing] = useState(false); + + const analyze = useCallback(() => { + setIsAnalyzing(true); + try { + const entries = getAuditEntries({ ...filters, limit: 1000 }); + const newReport = generateReport(entries, config); + setReport(newReport); + + // Also update the streaming engine + const engine = engineRef.current; + engine.reset(); + for (const entry of entries) { + engine.addEntry(entry); + } + } finally { + setIsAnalyzing(false); + } + }, [filters, config]); + + // Initial analysis and periodic refresh + useEffect(() => { + analyze(); + const interval = setInterval(analyze, analysisIntervalMs); + return () => clearInterval(interval); + }, [analyze, analysisIntervalMs]); + + // Subscribe to new audit entries for real-time updates + useEffect(() => { + const unsub = subscribeAudit((entry) => { + // Quick filter check before triggering full re-analysis + if (filters.category && entry.category !== filters.category) return; + if (filters.severity && entry.severity !== filters.severity) return; + + // Debounce: add to engine immediately, but batch re-analysis + engineRef.current.addEntry(entry); + }); + return unsub; + }, [filters]); + + const findings = useMemo(() => report.findings, [report]); + const riskScore = useMemo(() => report.riskScore, [report]); + + const filterEntries = useCallback( + (extraFilters) => getAuditEntries({ ...filters, ...extraFilters }), + [filters], + ); + + return { + report, + findings, + riskScore, + isAnalyzing, + refresh: analyze, + filterEntries, + }; +} + +export default useSecurityAnalysis; diff --git a/src/utils/securityAnalysis.js b/src/utils/securityAnalysis.js new file mode 100644 index 00000000..f2bb876a --- /dev/null +++ b/src/utils/securityAnalysis.js @@ -0,0 +1,812 @@ +/** + * Security Audit Trail Analysis Engine + * + * Provides AI-driven analysis of audit log data to identify: + * - Statistical anomalies (unusual event rates, off-hours activity) + * - Behavioral patterns (escalation chains, session drift, burst attacks) + * - Risk scoring with weighted severity classification + * - Actionable security reports with recommendations + * + * Design goals: + * - Pure functions operating on audit entry arrays (no side effects) + * - O(n) or O(n log n) complexity for real-time analysis + * - Thresholds and weights configurable per deployment + */ + +import { AuditCategory, AuditSeverity } from './audit.js'; + +// ─── Configuration ──────────────────────────────────────────────────────────── + +export const DEFAULT_CONFIG = Object.freeze({ + // Anomaly detection thresholds + rateStdDevMultiplier: 2.0, + burstWindowMs: 60_000, + burstThreshold: 10, + offHoursStart: 22, + offHoursEnd: 6, + minEntriesForBaselines: 20, + + // Pattern detection + escalationChainLength: 3, + sessionDriftThresholdMs: 24 * 60 * 60 * 1000, + maxSessionGapMs: 30 * 60 * 1000, + + // Risk scoring weights + weights: { + severity: { info: 0, low: 5, medium: 15, high: 35, critical: 60 }, + category: { + auth: 1.2, + wallet: 1.1, + transaction: 1.3, + contract: 1.0, + network: 0.6, + config: 0.8, + data_access: 1.1, + export: 0.9, + security: 1.5, + admin: 1.4, + system: 0.3, + }, + outcome: { success: 0, failure: 8, denied: 12 }, + }, + + // Composite score normalization + maxRiskScore: 100, +}); + +// ─── Severity Numeric Mapping ──────────────────────────────────────────────── + +const SEVERITY_NUMERIC = { + [AuditSeverity.INFO]: 0, + [AuditSeverity.LOW]: 1, + [AuditSeverity.MEDIUM]: 2, + [AuditSeverity.HIGH]: 3, + [AuditSeverity.CRITICAL]: 4, +}; + +// ─── Helper Utilities ──────────────────────────────────────────────────────── + +function ts(entry) { + return new Date(entry.timestamp).getTime(); +} + +function hourOfDay(entry) { + return new Date(entry.timestamp).getHours(); +} + +function mean(arr) { + if (!arr.length) return 0; + return arr.reduce((a, b) => a + b, 0) / arr.length; +} + +function stdDev(arr) { + if (arr.length < 2) return 0; + const m = mean(arr); + return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1)); +} + +function zScore(value, arr) { + const sd = stdDev(arr); + if (sd === 0) return value === arr[0] ? 0 : Infinity; + return (value - mean(arr)) / sd; +} + +function bucketEntries(entries, bucketMs) { + if (!entries.length) return []; + const sorted = entries.slice().sort((a, b) => ts(a) - ts(b)); + const start = ts(sorted[0]); + const end = ts(sorted[sorted.length - 1]); + const buckets = []; + for (let t = start; t <= end; t += bucketMs) { + const count = sorted.filter((e) => { + const et = ts(e); + return et >= t && et < t + bucketMs; + }).length; + buckets.push({ time: t, count }); + } + return buckets; +} + +// ─── Anomaly Detection ────────────────────────────────────────────────────── + +/** + * Detect rate-based anomalies using statistical deviation from the mean. + * Flags time buckets where event counts exceed the configured threshold. + * + * @param {object[]} entries - Audit log entries + * @param {object} [config] + * @returns {{ anomalies: object[], bucketData: object[] }} + */ +export function detectRateAnomalies(entries, config = DEFAULT_CONFIG) { + if (entries.length < config.minEntriesForBaselines) { + return { anomalies: [], bucketData: [] }; + } + + const bucketData = bucketEntries(entries, config.burstWindowMs); + const counts = bucketData.map((b) => b.count); + const m = mean(counts); + const sd = stdDev(counts); + const threshold = m + config.rateStdDevMultiplier * sd; + + const anomalies = bucketData + .filter((b) => b.count > threshold && b.count >= config.burstThreshold) + .map((b) => ({ + type: 'rate_spike', + severity: b.count > threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH, + timestamp: new Date(b.time).toISOString(), + count: b.count, + expectedMax: Math.round(threshold), + deviation: sd > 0 ? ((b.count - m) / sd).toFixed(1) : '∞', + message: `Event burst detected: ${b.count} events in window (baseline avg: ${m.toFixed(0)}, threshold: ${Math.round(threshold)})`, + })); + + return { anomalies, bucketData }; +} + +/** + * Detect off-hours activity (configurable quiet period). + * + * @param {object[]} entries + * @param {object} [config] + * @returns {object[]} + */ +export function detectOffHoursActivity(entries, config = DEFAULT_CONFIG) { + const { offHoursStart, offHoursEnd } = config; + const offHoursEntries = entries.filter((e) => { + const h = hourOfDay(e); + return offHoursStart > offHoursEnd + ? h >= offHoursStart || h < offHoursEnd + : h >= offHoursStart && h < offHoursEnd; + }); + + return offHoursEntries.map((e) => ({ + type: 'off_hours_activity', + severity: e.severity === AuditSeverity.CRITICAL || e.severity === AuditSeverity.HIGH + ? AuditSeverity.HIGH + : AuditSeverity.MEDIUM, + timestamp: e.timestamp, + entryId: e.id, + action: e.action, + actor: e.actor, + message: `Activity at unusual hour (${hourOfDay(e)}:00): ${e.action}`, + })); +} + +/** + * Detect actor velocity anomalies — an actor performing significantly more + * actions than their historical average. + * + * @param {object[]} entries + * @param {object} [config] + * @returns {object[]} + */ +export function detectActorVelocityAnomalies(entries, config = DEFAULT_CONFIG) { + if (entries.length < config.minEntriesForBaselines) { + return []; + } + + const sorted = entries.slice().sort((a, b) => ts(a) - ts(b)); + const windowMs = 10 * 60_000; + const actorCounts = new Map(); + + for (let i = 0; i < sorted.length; i++) { + const entry = sorted[i]; + const actor = entry.actor || 'anonymous'; + const now = ts(entry); + + if (!actorCounts.has(actor)) { + actorCounts.set(actor, []); + } + const history = actorCounts.get(actor); + + // Prune entries outside the sliding window + const windowStart = now - config.sessionDriftThresholdMs; + const recent = history.filter((h) => h.time >= windowStart); + recent.push(now); + actorCounts.set(actor, recent); + + // If we have enough history, check deviation + if (recent.length >= config.minEntriesForBaselines) { + const intervals = []; + for (let j = 1; j < recent.length; j++) { + intervals.push(recent[j] - recent[j - 1]); + } + if (intervals.length >= 3) { + const avgInterval = mean(intervals); + const currentGap = i > 0 ? now - ts(sorted[i - 1]) : Infinity; + if (currentGap < avgInterval * 0.2 && actor !== (sorted[i - 1]?.actor || 'anonymous')) { + // Skip — different actors + } + } + } + } + + // Aggregate actor totals and detect outliers + const actorTotals = new Map(); + for (const entry of entries) { + const actor = entry.actor || 'anonymous'; + actorTotals.set(actor, (actorTotals.get(actor) || 0) + 1); + } + + const totals = [...actorTotals.values()]; + if (totals.length < 2) return []; + + const m = mean(totals); + const sd = stdDev(totals); + if (sd === 0) return []; + const threshold = m + config.rateStdDevMultiplier * sd; + + const anomalies = []; + for (const [actor, count] of actorTotals) { + if (count > threshold) { + anomalies.push({ + type: 'actor_velocity', + severity: count > threshold * 2 ? AuditSeverity.HIGH : AuditSeverity.MEDIUM, + actor, + totalActions: count, + expectedMax: Math.round(threshold), + deviation: ((count - m) / sd).toFixed(1), + message: `Actor "${actor}" performed ${count} actions (expected max: ~${Math.round(threshold)})`, + }); + } + } + + return anomalies; +} + +/** + * Detect unique-IP / unique-actor bursts — many different actors appearing + * in a short window (potential coordinated attack or bot activity). + * + * @param {object[]} entries + * @param {object} [config] + * @returns {object[]} + */ +export function detectMultiActorBursts(entries, config = DEFAULT_CONFIG) { + const windowMs = config.burstWindowMs; + const sorted = entries.slice().sort((a, b) => ts(a) - ts(b)); + const anomalies = []; + + for (let i = 0; i < sorted.length; i++) { + const windowEnd = ts(sorted[i]); + const windowStart = windowEnd - windowMs; + const inWindow = sorted.filter((e) => { + const t = ts(e); + return t >= windowStart && t <= windowEnd; + }); + + const uniqueActors = new Set(inWindow.map((e) => e.actor || 'anonymous')); + if (uniqueActors.size >= config.burstThreshold) { + anomalies.push({ + type: 'multi_actor_burst', + severity: AuditSeverity.HIGH, + timestamp: sorted[i].timestamp, + uniqueActors: uniqueActors.size, + totalEvents: inWindow.length, + actors: [...uniqueActors].slice(0, 5), + message: `${uniqueActors.size} unique actors in ${windowMs / 1000}s window (${inWindow.length} events)`, + }); + // Skip ahead to avoid re-flagging the same window + i = sorted.findIndex((e) => ts(e) > windowEnd); + if (i === -1) break; + } + } + + return anomalies; +} + +// ─── Pattern Recognition ──────────────────────────────────────────────────── + +/** + * Detect escalation chains — sequences where severity increases over time + * for the same actor or category (e.g., info → medium → critical). + * + * @param {object[]} entries + * @param {object} [config] + * @returns {object[]} + */ +export function detectEscalationChains(entries, config = DEFAULT_CONFIG) { + const sorted = entries.slice().sort((a, b) => ts(a) - ts(b)); + const minLen = config.escalationChainLength; + const chains = []; + + // Group by actor + const byActor = new Map(); + for (const e of sorted) { + const actor = e.actor || 'anonymous'; + if (!byActor.has(actor)) byActor.set(actor, []); + byActor.get(actor).push(e); + } + + for (const [actor, actorEntries] of byActor) { + if (actorEntries.length < minLen) continue; + + let currentChain = [actorEntries[0]]; + for (let i = 1; i < actorEntries.length; i++) { + const prev = SEVERITY_NUMERIC[currentChain[currentChain.length - 1].severity] ?? 0; + const curr = SEVERITY_NUMERIC[actorEntries[i].severity] ?? 0; + + if (curr >= prev && ts(actorEntries[i]) - ts(currentChain[currentChain.length - 1]) < config.sessionDriftThresholdMs) { + currentChain.push(actorEntries[i]); + } else { + if (currentChain.length >= minLen) { + chains.push({ + type: 'escalation_chain', + severity: AuditSeverity.HIGH, + actor, + chainLength: currentChain.length, + fromSeverity: currentChain[0].severity, + toSeverity: currentChain[currentChain.length - 1].severity, + startTime: currentChain[0].timestamp, + endTime: currentChain[currentChain.length - 1].timestamp, + actions: currentChain.map((e) => e.action), + message: `Escalation chain: ${actor} went from ${currentChain[0].severity} → ${currentChain[currentChain.length - 1].severity} over ${currentChain.length} events`, + }); + } + currentChain = [actorEntries[i]]; + } + } + if (currentChain.length >= minLen) { + chains.push({ + type: 'escalation_chain', + severity: AuditSeverity.HIGH, + actor, + chainLength: currentChain.length, + fromSeverity: currentChain[0].severity, + toSeverity: currentChain[currentChain.length - 1].severity, + startTime: currentChain[0].timestamp, + endTime: currentChain[currentChain.length - 1].timestamp, + actions: currentChain.map((e) => e.action), + message: `Escalation chain: ${actor} went from ${currentChain[0].severity} → ${currentChain[currentChain.length - 1].severity} over ${currentChain.length} events`, + }); + } + } + + return chains; +} + +/** + * Detect repeated-failure patterns — the same action failing multiple times + * in succession (may indicate attack, misconfiguration, or stuck process). + * + * @param {object[]} entries + * @param {number} [threshold=3] - Minimum consecutive failures to flag + * @returns {object[]} + */ +export function detectRepeatedFailures(entries, threshold = 3) { + const sorted = entries.slice().sort((a, b) => ts(a) - ts(b)); + const failures = sorted.filter((e) => e.outcome === 'failure' || e.outcome === 'denied'); + const patterns = []; + + if (failures.length < threshold) return patterns; + + let streak = [failures[0]]; + for (let i = 1; i < failures.length; i++) { + const gap = ts(failures[i]) - ts(streak[streak.length - 1]); + if (failures[i].action === streak[0].action && + failures[i].actor === streak[0].actor && + gap < 10 * 60_000) { + streak.push(failures[i]); + } else { + if (streak.length >= threshold) { + patterns.push({ + type: 'repeated_failures', + severity: streak.length >= threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH, + action: streak[0].action, + actor: streak[0].actor, + count: streak.length, + startTime: streak[0].timestamp, + endTime: streak[streak.length - 1].timestamp, + message: `${streak.length} consecutive failures for "${streak[0].action}" by ${streak[0].actor || 'anonymous'}`, + }); + } + streak = [failures[i]]; + } + } + if (streak.length >= threshold) { + patterns.push({ + type: 'repeated_failures', + severity: streak.length >= threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH, + action: streak[0].action, + actor: streak[0].actor, + count: streak.length, + startTime: streak[0].timestamp, + endTime: streak[streak.length - 1].timestamp, + message: `${streak.length} consecutive failures for "${streak[0].action}" by ${streak[0].actor || 'anonymous'}`, + }); + } + + return patterns; +} + +/** + * Detect session drift — a session lasting significantly longer than expected, + * or entries attributed to the same session with large time gaps. + * + * @param {object[]} entries + * @param {object} [config] + * @returns {object[]} + */ +export function detectSessionDrift(entries, config = DEFAULT_CONFIG) { + const bySession = new Map(); + for (const e of entries) { + const sid = e.sessionId; + if (!sid) continue; + if (!bySession.has(sid)) bySession.set(sid, []); + bySession.get(sid).push(e); + } + + const anomalies = []; + for (const [sid, sessionEntries] of bySession) { + if (sessionEntries.length < 2) continue; + const sorted = sessionEntries.slice().sort((a, b) => ts(a) - ts(b)); + const duration = ts(sorted[sorted.length - 1]) - ts(sorted[0]); + + if (duration > config.sessionDriftThresholdMs) { + anomalies.push({ + type: 'session_drift', + severity: AuditSeverity.MEDIUM, + sessionId: sid, + durationMs: duration, + durationHuman: formatDuration(duration), + entryCount: sorted.length, + startTime: sorted[0].timestamp, + endTime: sorted[sorted.length - 1].timestamp, + message: `Session ${sid.slice(0, 12)}… lasted ${formatDuration(duration)} (${sorted.length} events)`, + }); + } + + // Check for suspiciously large gaps within a session + for (let i = 1; i < sorted.length; i++) { + const gap = ts(sorted[i]) - ts(sorted[i - 1]); + if (gap > config.maxSessionGapMs && gap < config.sessionDriftThresholdMs) { + anomalies.push({ + type: 'session_gap', + severity: AuditSeverity.LOW, + sessionId: sid, + gapMs: gap, + gapHuman: formatDuration(gap), + beforeAction: sorted[i - 1].action, + afterAction: sorted[i].action, + message: `Gap of ${formatDuration(gap)} in session ${sid.slice(0, 12)}… between "${sorted[i - 1].action}" and "${sorted[i].action}"`, + }); + } + } + } + + return anomalies; +} + +/** + * Detect category concentration — an unusually high proportion of events + * in a single category (may indicate targeted attack or misconfiguration). + * + * @param {object[]} entries + * @param {number} [concentrationThreshold=0.6] - Min ratio to flag + * @returns {object[]} + */ +export function detectCategoryConcentration(entries, concentrationThreshold = 0.6) { + if (entries.length < 10) return []; + + const byCategory = new Map(); + for (const e of entries) { + byCategory.set(e.category, (byCategory.get(e.category) || 0) + 1); + } + + const anomalies = []; + for (const [category, count] of byCategory) { + const ratio = count / entries.length; + if (ratio >= concentrationThreshold) { + anomalies.push({ + type: 'category_concentration', + severity: ratio > 0.8 ? AuditSeverity.HIGH : AuditSeverity.MEDIUM, + category, + count, + ratio: ratio.toFixed(2), + total: entries.length, + message: `${ratio.toFixed(0)}% of events (${count}/${entries.length}) are in category "${category}"`, + }); + } + } + + return anomalies; +} + +// ─── Risk Scoring ─────────────────────────────────────────────────────────── + +/** + * Compute a composite risk score for a single audit entry. + * + * @param {object} entry + * @param {object} [config] + * @returns {number} 0–100 + */ +export function scoreEntry(entry, config = DEFAULT_CONFIG) { + const { weights, maxRiskScore } = config; + + const severityScore = weights.severity[entry.severity] ?? 0; + const categoryMultiplier = weights.category[entry.category] ?? 1.0; + const outcomeScore = weights.outcome[entry.outcome] ?? 0; + + const raw = (severityScore + outcomeScore) * categoryMultiplier; + return Math.min(maxRiskScore, Math.round(raw)); +} + +/** + * Compute an aggregate risk score for a set of audit entries. + * Uses the maximum individual score weighted by anomaly density. + * + * @param {object[]} entries + * @param {object[]} [anomalies=[]] + * @param {object} [config] + * @returns {{ score: number, grade: string, breakdown: object }} + */ +export function computeAggregateRisk(entries, anomalies = [], config = DEFAULT_CONFIG) { + if (!entries.length) { + return { score: 0, grade: 'A', breakdown: { severity: 0, anomalies: 0, outcomes: 0 } }; + } + + const scores = entries.map((e) => scoreEntry(e, config)); + const maxEntryScore = Math.max(...scores); + const avgEntryScore = mean(scores); + + // Anomaly density contribution + const anomalyDensity = anomalies.length / Math.max(1, entries.length); + const anomalyScore = Math.min(40, anomalyDensity * 100); + + // Failure rate contribution + const failures = entries.filter((e) => e.outcome === 'failure' || e.outcome === 'denied').length; + const failureRate = failures / entries.length; + const outcomeScore = Math.min(30, failureRate * 60); + + // Composite score: weighted blend + const composite = Math.min( + config.maxRiskScore, + Math.round(maxEntryScore * 0.4 + avgEntryScore * 0.2 + anomalyScore * 0.25 + outcomeScore * 0.15), + ); + + const grade = riskGrade(composite); + + return { + score: composite, + grade, + breakdown: { + severity: Math.round(maxEntryScore), + anomalies: Math.round(anomalyScore), + outcomes: Math.round(outcomeScore), + }, + }; +} + +/** + * Map a numeric risk score to a letter grade. + * + * @param {number} score + * @returns {string} + */ +export function riskGrade(score) { + if (score <= 10) return 'A'; + if (score <= 25) return 'B'; + if (score <= 50) return 'C'; + if (score <= 75) return 'D'; + return 'F'; +} + +// ─── Report Generation ────────────────────────────────────────────────────── + +/** + * Recommendation templates keyed by anomaly/pattern type. + */ +const RECOMMENDATIONS = { + rate_spike: [ + 'Investigate the source of the event burst.', + 'Consider temporarily rate-limiting affected endpoints.', + 'Verify no automated process is malfunctioning.', + ], + off_hours_activity: [ + 'Verify the actor was authorized for this time window.', + 'Review if off-hours access aligns with on-call schedules.', + 'Consider enforcing stricter MFA for off-hours operations.', + ], + actor_velocity: [ + 'Review the actor\'s recent activity for legitimacy.', + 'Check for compromised credentials or session tokens.', + 'Consider imposing per-actor rate limits.', + ], + multi_actor_burst: [ + 'Check for coordinated attack or bot activity.', + 'Review IP addresses and geolocation of actors.', + 'Consider enabling additional CAPTCHA or challenge mechanisms.', + ], + escalation_chain: [ + 'Investigate the root cause of the escalating severity.', + 'Ensure alerting is configured for critical-severity events.', + 'Consider automated containment for rapid escalations.', + ], + repeated_failures: [ + 'Check for misconfiguration or service degradation.', + 'Review if the actor\'s permissions are correctly assigned.', + 'Investigate if this is a brute-force or fuzzing attempt.', + ], + session_drift: [ + 'Verify session belongs to a legitimate long-running operation.', + 'Check for session fixation or hijacking indicators.', + 'Consider implementing session timeout policies.', + ], + session_gap: [ + 'Review what happened during the gap period.', + 'Check for network interruptions or suspended sessions.', + ], + category_concentration: [ + 'Investigate why one category dominates activity.', + 'Check for automated scripts or misconfigured integrations.', + 'Review access controls for the concentrated category.', + ], +}; + +/** + * Generate a comprehensive security analysis report. + * + * @param {object[]} entries - Audit log entries to analyze + * @param {object} [config] + * @returns {object} Full report with findings, risk score, and recommendations + */ +export function generateReport(entries, config = DEFAULT_CONFIG) { + if (!entries.length) { + return { + timestamp: new Date().toISOString(), + entryCount: 0, + riskScore: { score: 0, grade: 'A', breakdown: { severity: 0, anomalies: 0, outcomes: 0 } }, + findings: [], + summary: { totalFindings: 0, byType: {}, bySeverity: {} }, + recommendations: [], + }; + } + + // Run all detectors + const { anomalies: rateAnomalies } = detectRateAnomalies(entries, config); + const offHours = detectOffHoursActivity(entries, config); + const velocityAnomalies = detectActorVelocityAnomalies(entries, config); + const multiActorBursts = detectMultiActorBursts(entries, config); + const escalationChains = detectEscalationChains(entries, config); + const repeatedFailures = detectRepeatedFailures(entries); + const sessionDrift = detectSessionDrift(entries, config); + const categoryConcentration = detectCategoryConcentration(entries); + + const allFindings = [ + ...rateAnomalies, + ...offHours, + ...velocityAnomalies, + ...multiActorBursts, + ...escalationChains, + ...repeatedFailures, + ...sessionDrift, + ...categoryConcentration, + ].sort((a, b) => { + const sa = SEVERITY_NUMERIC[a.severity] ?? 0; + const sb = SEVERITY_NUMERIC[b.severity] ?? 0; + return sb - sa; + }); + + // Compute aggregate risk + const riskScore = computeAggregateRisk(entries, allFindings, config); + + // Aggregate by type + const byType = {}; + const bySeverity = {}; + for (const f of allFindings) { + byType[f.type] = (byType[f.type] || 0) + 1; + bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1; + } + + // Collect unique recommendations + const recommendationSet = new Set(); + for (const f of allFindings) { + const recs = RECOMMENDATIONS[f.type] || []; + for (const r of recs) recommendationSet.add(r); + } + + // Basic entry-level stats + const entriesBySeverity = {}; + const entriesByCategory = {}; + const entriesByOutcome = {}; + for (const e of entries) { + entriesBySeverity[e.severity] = (entriesBySeverity[e.severity] || 0) + 1; + entriesByCategory[e.category] = (entriesByCategory[e.category] || 0) + 1; + entriesByOutcome[e.outcome] = (entriesByOutcome[e.outcome] || 0) + 1; + } + + return { + timestamp: new Date().toISOString(), + entryCount: entries.length, + timeRange: { + from: entries.reduce((min, e) => e.timestamp < min ? e.timestamp : min, entries[0].timestamp), + to: entries.reduce((max, e) => e.timestamp > max ? e.timestamp : max, entries[0].timestamp), + }, + riskScore, + findings: allFindings, + summary: { + totalFindings: allFindings.length, + byType, + bySeverity, + }, + entryStats: { + bySeverity: entriesBySeverity, + byCategory: entriesByCategory, + byOutcome: entriesByOutcome, + }, + recommendations: [...recommendationSet], + }; +} + +// ─── Real-time Analysis Engine ────────────────────────────────────────────── + +/** + * Create a streaming analysis engine that maintains rolling windows + * and emits findings as new entries arrive. + * + * @param {object} [config] + * @returns {{ addEntry, getFindings, getRiskScore, getReport, reset }} + */ +export function createStreamingEngine(config = DEFAULT_CONFIG) { + let buffer = []; + let cachedFindings = null; + let cachedReport = null; + + function addEntry(entry) { + buffer.push(entry); + // Keep buffer bounded + if (buffer.length > 2000) { + buffer = buffer.slice(-1500); + } + // Invalidate caches + cachedFindings = null; + cachedReport = null; + } + + function getFindings() { + if (!cachedFindings) { + const { anomalies } = detectRateAnomalies(buffer, config); + cachedFindings = [ + ...anomalies, + ...detectOffHoursActivity(buffer, config), + ...detectActorVelocityAnomalies(buffer, config), + ...detectMultiActorBursts(buffer, config), + ...detectEscalationChains(buffer, config), + ...detectRepeatedFailures(buffer), + ...detectSessionDrift(buffer, config), + ...detectCategoryConcentration(buffer), + ].sort((a, b) => (SEVERITY_NUMERIC[b.severity] ?? 0) - (SEVERITY_NUMERIC[a.severity] ?? 0)); + } + return cachedFindings; + } + + function getRiskScore() { + return computeAggregateRisk(buffer, getFindings(), config); + } + + function getReport() { + if (!cachedReport) { + cachedReport = generateReport(buffer, config); + } + return cachedReport; + } + + function reset() { + buffer = []; + cachedFindings = null; + cachedReport = null; + } + + return { addEntry, getFindings, getRiskScore, getReport, reset, get buffer() { return buffer; } }; +} + +// ─── Internal Helpers ─────────────────────────────────────────────────────── + +function formatDuration(ms) { + if (ms < 1000) return `${ms}ms`; + if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`; + if (ms < 3600_000) return `${(ms / 60_000).toFixed(0)}m`; + if (ms < 86400_000) return `${(ms / 3600_000).toFixed(1)}h`; + return `${(ms / 86400_000).toFixed(1)}d`; +} diff --git a/tests/unit/utils/securityAnalysis.test.js b/tests/unit/utils/securityAnalysis.test.js new file mode 100644 index 00000000..89623c4e --- /dev/null +++ b/tests/unit/utils/securityAnalysis.test.js @@ -0,0 +1,758 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('../../../src/utils/audit.js', () => ({ + AuditCategory: { + AUTH: 'auth', + WALLET: 'wallet', + TRANSACTION: 'transaction', + CONTRACT: 'contract', + NETWORK: 'network', + CONFIG: 'config', + DATA_ACCESS: 'data_access', + EXPORT: 'export', + SECURITY: 'security', + ADMIN: 'admin', + SYSTEM: 'system', + }, + AuditSeverity: { + INFO: 'info', + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high', + CRITICAL: 'critical', + }, +})); + +import { + detectRateAnomalies, + detectOffHoursActivity, + detectActorVelocityAnomalies, + detectMultiActorBursts, + detectEscalationChains, + detectRepeatedFailures, + detectSessionDrift, + detectCategoryConcentration, + scoreEntry, + computeAggregateRisk, + riskGrade, + generateReport, + createStreamingEngine, + DEFAULT_CONFIG, +} from '../../../src/utils/securityAnalysis.js'; + +// ─── Test Factories ───────────────────────────────────────────────────────── + +function makeEntry(overrides = {}) { + return { + id: `test-${Math.random().toString(36).slice(2, 8)}`, + timestamp: new Date().toISOString(), + action: 'auth.login.success', + category: 'auth', + severity: 'info', + actor: 'user-A', + target: null, + outcome: 'success', + metadata: {}, + sessionId: 'session-1', + hash: 'abc123', + prevHash: '0', + ...overrides, + }; +} + +function makeEntries(count, overrides = {}) { + const base = makeEntry(overrides); + return Array.from({ length: count }, (_, i) => ({ + ...base, + id: `test-${i}`, + timestamp: new Date(Date.now() + i * 1000).toISOString(), + })); +} + +function makeTimeShiftedEntries(count, intervalMs, actorFn, severityFn, overrides = {}) { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + return Array.from({ length: count }, (_, i) => makeEntry({ + ...overrides, + timestamp: new Date(start + i * intervalMs).toISOString(), + actor: typeof actorFn === 'function' ? actorFn(i) : actorFn, + severity: typeof severityFn === 'function' ? severityFn(i) : severityFn, + })); +} + +// ─── detectRateAnomalies ──────────────────────────────────────────────────── + +describe('detectRateAnomalies', () => { + it('returns empty when below minimum entries threshold', () => { + const entries = makeEntries(5); + const result = detectRateAnomalies(entries); + expect(result.anomalies).toHaveLength(0); + expect(result.bucketData).toHaveLength(0); + }); + + it('returns empty for uniform event distribution', () => { + const entries = makeEntries(30); + const result = detectRateAnomalies(entries); + expect(result.anomalies).toHaveLength(0); + expect(result.bucketData.length).toBeGreaterThan(0); + }); + + it('detects rate spikes when a bucket far exceeds the mean', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = []; + // Spread 25 events across 10 buckets (2-3 per bucket) + for (let i = 0; i < 25; i++) { + entries.push(makeEntry({ + timestamp: new Date(start + i * 200_000).toISOString(), + })); + } + // Dump 15 events into one 60s bucket + for (let i = 0; i < 15; i++) { + entries.push(makeEntry({ + timestamp: new Date(start + 1_000_000 + i * 1000).toISOString(), + })); + } + const result = detectRateAnomalies(entries); + expect(result.anomalies.length).toBeGreaterThanOrEqual(1); + expect(result.anomalies[0].type).toBe('rate_spike'); + expect(result.anomalies[0].count).toBeGreaterThanOrEqual(15); + }); + + it('uses custom config thresholds', () => { + const entries = makeEntries(25); + const result = detectRateAnomalies(entries, { + ...DEFAULT_CONFIG, + minEntriesForBaselines: 5, + burstThreshold: 2, + }); + // Even uniform data may not trigger, but should not error + expect(result).toHaveProperty('anomalies'); + expect(result).toHaveProperty('bucketData'); + }); +}); + +// ─── detectOffHoursActivity ───────────────────────────────────────────────── + +describe('detectOffHoursActivity', () => { + it('detects activity during off-hours (22:00-06:00)', () => { + const entries = [ + makeEntry({ timestamp: '2026-01-01T23:00:00Z', action: 'tx.submit', severity: 'info' }), + makeEntry({ timestamp: '2026-01-01T03:00:00Z', action: 'wallet.connect', severity: 'info' }), + makeEntry({ timestamp: '2026-01-01T12:00:00Z', action: 'auth.login', severity: 'info' }), + ]; + const result = detectOffHoursActivity(entries); + expect(result).toHaveLength(2); + expect(result[0].type).toBe('off_hours_activity'); + expect(result[1].type).toBe('off_hours_activity'); + }); + + it('returns empty for all business-hours activity', () => { + const entries = [ + makeEntry({ timestamp: '2026-01-01T09:00:00Z' }), + makeEntry({ timestamp: '2026-01-01T14:00:00Z' }), + makeEntry({ timestamp: '2026-01-01T17:00:00Z' }), + ]; + const result = detectOffHoursActivity(entries); + expect(result).toHaveLength(0); + }); + + it('returns empty for empty input', () => { + expect(detectOffHoursActivity([])).toHaveLength(0); + }); + + it('grades critical/high severity entries as HIGH in off-hours', () => { + const entries = [ + makeEntry({ timestamp: '2026-01-01T23:30:00Z', severity: 'critical' }), + ]; + const result = detectOffHoursActivity(entries); + expect(result[0].severity).toBe('high'); + }); + + it('grades other severity entries as MEDIUM in off-hours', () => { + const entries = [ + makeEntry({ timestamp: '2026-01-01T23:30:00Z', severity: 'low' }), + ]; + const result = detectOffHoursActivity(entries); + expect(result[0].severity).toBe('medium'); + }); +}); + +// ─── detectActorVelocityAnomalies ─────────────────────────────────────────── + +describe('detectActorVelocityAnomalies', () => { + it('returns empty when below minimum entries', () => { + const entries = makeEntries(5); + expect(detectActorVelocityAnomalies(entries)).toHaveLength(0); + }); + + it('returns empty when all actors have similar counts', () => { + // Create entries evenly distributed across 5 actors + const entries = []; + for (let a = 0; a < 5; a++) { + for (let i = 0; i < 6; i++) { + entries.push(makeEntry({ actor: `actor-${a}` })); + } + } + expect(detectActorVelocityAnomalies(entries)).toHaveLength(0); + }); + + it('detects an actor with significantly more actions', () => { + const entries = []; + // Normal actors: 3 actions each + for (let a = 0; a < 5; a++) { + for (let i = 0; i < 3; i++) { + entries.push(makeEntry({ actor: `normal-${a}` })); + } + } + // Overactive actor: 30 actions + for (let i = 0; i < 30; i++) { + entries.push(makeEntry({ actor: 'overactive' })); + } + const result = detectActorVelocityAnomalies(entries); + expect(result.length).toBeGreaterThanOrEqual(1); + const overactive = result.find((r) => r.actor === 'overactive'); + expect(overactive).toBeDefined(); + expect(overactive.totalActions).toBe(30); + }); +}); + +// ─── detectMultiActorBursts ───────────────────────────────────────────────── + +describe('detectMultiActorBursts', () => { + it('detects many unique actors in a short window', () => { + const start = Date.now(); + const entries = []; + for (let i = 0; i < 12; i++) { + entries.push(makeEntry({ + actor: `attacker-${i}`, + timestamp: new Date(start + i * 1000).toISOString(), + })); + } + const result = detectMultiActorBursts(entries, { + ...DEFAULT_CONFIG, + burstThreshold: 10, + }); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result[0].type).toBe('multi_actor_burst'); + expect(result[0].uniqueActors).toBeGreaterThanOrEqual(10); + }); + + it('returns empty when actors are spread out', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = []; + for (let i = 0; i < 12; i++) { + entries.push(makeEntry({ + actor: `user-${i}`, + timestamp: new Date(start + i * 120_000).toISOString(), // 2min apart + })); + } + const result = detectMultiActorBursts(entries); + expect(result).toHaveLength(0); + }); + + it('returns empty for single actor', () => { + const entries = makeEntries(15, { actor: 'same-actor' }); + expect(detectMultiActorBursts(entries)).toHaveLength(0); + }); +}); + +// ─── detectEscalationChains ───────────────────────────────────────────────── + +describe('detectEscalationChains', () => { + it('detects increasing severity for same actor', () => { + const entries = makeTimeShiftedEntries(4, 1000, 'attacker', (i) => { + return ['info', 'medium', 'high', 'critical'][i]; + }); + const result = detectEscalationChains(entries, { + ...DEFAULT_CONFIG, + escalationChainLength: 3, + }); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result[0].type).toBe('escalation_chain'); + expect(result[0].actor).toBe('attacker'); + expect(result[0].fromSeverity).toBe('info'); + expect(result[0].toSeverity).toBe('critical'); + }); + + it('returns empty when severity strictly decreases', () => { + const entries = makeTimeShiftedEntries(4, 1000, 'user', (i) => { + return ['critical', 'high', 'medium', 'info'][i]; + }); + const result = detectEscalationChains(entries); + expect(result).toHaveLength(0); + }); + + it('returns empty when chain is shorter than minimum', () => { + const entries = makeTimeShiftedEntries(2, 1000, 'user', (i) => { + return ['info', 'critical'][i]; + }); + const result = detectEscalationChains(entries, { + ...DEFAULT_CONFIG, + escalationChainLength: 3, + }); + expect(result).toHaveLength(0); + }); + + it('detects multiple chains for different actors', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = [ + makeEntry({ actor: 'a1', severity: 'info', timestamp: new Date(start).toISOString() }), + makeEntry({ actor: 'a1', severity: 'medium', timestamp: new Date(start + 1000).toISOString() }), + makeEntry({ actor: 'a1', severity: 'critical', timestamp: new Date(start + 2000).toISOString() }), + makeEntry({ actor: 'a2', severity: 'low', timestamp: new Date(start).toISOString() }), + makeEntry({ actor: 'a2', severity: 'high', timestamp: new Date(start + 1000).toISOString() }), + makeEntry({ actor: 'a2', severity: 'critical', timestamp: new Date(start + 2000).toISOString() }), + ]; + const result = detectEscalationChains(entries); + expect(result.length).toBeGreaterThanOrEqual(2); + }); +}); + +// ─── detectRepeatedFailures ───────────────────────────────────────────────── + +describe('detectRepeatedFailures', () => { + it('detects consecutive failures for same action and actor', () => { + const start = Date.now(); + const entries = []; + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + action: 'tx.submit', + actor: 'user-1', + outcome: 'failure', + timestamp: new Date(start + i * 5000).toISOString(), + })); + } + const result = detectRepeatedFailures(entries); + expect(result).toHaveLength(1); + expect(result[0].type).toBe('repeated_failures'); + expect(result[0].count).toBe(5); + expect(result[0].action).toBe('tx.submit'); + }); + + it('returns empty when fewer than threshold failures', () => { + const entries = [ + makeEntry({ outcome: 'failure', action: 'auth.login' }), + makeEntry({ outcome: 'failure', action: 'auth.login' }), + ]; + expect(detectRepeatedFailures(entries, 3)).toHaveLength(0); + }); + + it('returns empty when failures are for different actions', () => { + const start = Date.now(); + const entries = [ + makeEntry({ outcome: 'failure', action: 'auth.login', timestamp: new Date(start).toISOString() }), + makeEntry({ outcome: 'failure', action: 'tx.submit', timestamp: new Date(start + 1000).toISOString() }), + makeEntry({ outcome: 'failure', action: 'auth.login', timestamp: new Date(start + 2000).toISOString() }), + ]; + expect(detectRepeatedFailures(entries, 3)).toHaveLength(0); + }); + + it('returns empty when failures are too far apart', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = [ + makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start).toISOString() }), + makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start + 20 * 60_000).toISOString() }), + makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start + 40 * 60_000).toISOString() }), + ]; + expect(detectRepeatedFailures(entries, 3)).toHaveLength(0); + }); + + it('grades severity as CRITICAL when count >= threshold * 2', () => { + const start = Date.now(); + const entries = []; + for (let i = 0; i < 6; i++) { + entries.push(makeEntry({ + outcome: 'denied', + action: 'admin.delete', + actor: 'admin', + timestamp: new Date(start + i * 2000).toISOString(), + })); + } + const result = detectRepeatedFailures(entries, 3); + expect(result[0].severity).toBe('critical'); + }); +}); + +// ─── detectSessionDrift ───────────────────────────────────────────────────── + +describe('detectSessionDrift', () => { + it('detects session lasting longer than threshold', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = [ + makeEntry({ sessionId: 'sess-long', timestamp: new Date(start).toISOString() }), + makeEntry({ sessionId: 'sess-long', timestamp: new Date(start + 25 * 3600_000).toISOString() }), + ]; + const result = detectSessionDrift(entries); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result[0].type).toBe('session_drift'); + expect(result[0].sessionId).toBe('sess-long'); + }); + + it('detects large gaps within a session', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = [ + makeEntry({ sessionId: 'sess-gap', action: 'auth.login', timestamp: new Date(start).toISOString() }), + makeEntry({ sessionId: 'sess-gap', action: 'tx.submit', timestamp: new Date(start + 45 * 60_000).toISOString() }), + ]; + const result = detectSessionDrift(entries); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result.some((r) => r.type === 'session_gap')).toBe(true); + }); + + it('returns empty for entries without sessionId', () => { + const entries = [ + makeEntry({ sessionId: undefined }), + makeEntry({ sessionId: undefined }), + ]; + expect(detectSessionDrift(entries)).toHaveLength(0); + }); + + it('returns empty for short sessions', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = [ + makeEntry({ sessionId: 'short', timestamp: new Date(start).toISOString() }), + makeEntry({ sessionId: 'short', timestamp: new Date(start + 60_000).toISOString() }), + ]; + expect(detectSessionDrift(entries)).toHaveLength(0); + }); +}); + +// ─── detectCategoryConcentration ──────────────────────────────────────────── + +describe('detectCategoryConcentration', () => { + it('detects when one category dominates', () => { + const entries = []; + for (let i = 0; i < 18; i++) entries.push(makeEntry({ category: 'security' })); + for (let i = 0; i < 2; i++) entries.push(makeEntry({ category: 'auth' })); + const result = detectCategoryConcentration(entries); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result[0].type).toBe('category_concentration'); + expect(result[0].category).toBe('security'); + }); + + it('returns empty for balanced distribution', () => { + const entries = []; + const cats = ['auth', 'transaction', 'security', 'wallet', 'system']; + for (let i = 0; i < 20; i++) { + entries.push(makeEntry({ category: cats[i % cats.length] })); + } + expect(detectCategoryConcentration(entries)).toHaveLength(0); + }); + + it('returns empty when fewer than 10 entries', () => { + const entries = makeEntries(5, { category: 'security' }); + expect(detectCategoryConcentration(entries)).toHaveLength(0); + }); +}); + +// ─── scoreEntry ────────────────────────────────────────────────────────────── + +describe('scoreEntry', () => { + it('returns 0 for info/success/system entry', () => { + const entry = makeEntry({ severity: 'info', outcome: 'success', category: 'system' }); + expect(scoreEntry(entry)).toBe(0); + }); + + it('returns higher score for critical severity', () => { + const crit = makeEntry({ severity: 'critical', outcome: 'success', category: 'system' }); + const info = makeEntry({ severity: 'info', outcome: 'success', category: 'system' }); + expect(scoreEntry(crit)).toBeGreaterThan(scoreEntry(info)); + }); + + it('applies category multiplier', () => { + const security = makeEntry({ severity: 'medium', outcome: 'success', category: 'security' }); + const system = makeEntry({ severity: 'medium', outcome: 'success', category: 'system' }); + expect(scoreEntry(security)).toBeGreaterThan(scoreEntry(system)); + }); + + it('adds outcome penalty for failures', () => { + const success = makeEntry({ severity: 'high', outcome: 'success', category: 'auth' }); + const failure = makeEntry({ severity: 'high', outcome: 'failure', category: 'auth' }); + expect(scoreEntry(failure)).toBeGreaterThan(scoreEntry(success)); + }); + + it('caps at maxRiskScore', () => { + const entry = makeEntry({ severity: 'critical', outcome: 'denied', category: 'security' }); + expect(scoreEntry(entry)).toBeLessThanOrEqual(100); + }); +}); + +// ─── computeAggregateRisk ─────────────────────────────────────────────────── + +describe('computeAggregateRisk', () => { + it('returns score 0 / grade A for empty entries', () => { + const result = computeAggregateRisk([]); + expect(result.score).toBe(0); + expect(result.grade).toBe('A'); + }); + + it('returns higher score with more anomalies', () => { + const entries = makeEntries(10, { severity: 'medium', outcome: 'success', category: 'auth' }); + const low = computeAggregateRisk(entries, []); + const high = computeAggregateRisk(entries, Array.from({ length: 5 }, () => ({ type: 'test', severity: 'high' }))); + expect(high.score).toBeGreaterThanOrEqual(low.score); + }); + + it('returns higher score with more failures', () => { + const successEntries = makeEntries(10, { outcome: 'success' }); + const failureEntries = makeEntries(10, { outcome: 'failure' }); + expect(computeAggregateRisk(failureEntries).score).toBeGreaterThan( + computeAggregateRisk(successEntries).score, + ); + }); + + it('returns valid grades', () => { + const entries = makeEntries(10, { severity: 'critical', outcome: 'denied', category: 'security' }); + const result = computeAggregateRisk(entries); + expect(['A', 'B', 'C', 'D', 'F']).toContain(result.grade); + }); +}); + +// ─── riskGrade ────────────────────────────────────────────────────────────── + +describe('riskGrade', () => { + it('returns A for scores 0-10', () => { + expect(riskGrade(0)).toBe('A'); + expect(riskGrade(10)).toBe('A'); + }); + + it('returns B for scores 11-25', () => { + expect(riskGrade(11)).toBe('B'); + expect(riskGrade(25)).toBe('B'); + }); + + it('returns C for scores 26-50', () => { + expect(riskGrade(26)).toBe('C'); + expect(riskGrade(50)).toBe('C'); + }); + + it('returns D for scores 51-75', () => { + expect(riskGrade(51)).toBe('D'); + expect(riskGrade(75)).toBe('D'); + }); + + it('returns F for scores 76-100', () => { + expect(riskGrade(76)).toBe('F'); + expect(riskGrade(100)).toBe('F'); + }); +}); + +// ─── generateReport ───────────────────────────────────────────────────────── + +describe('generateReport', () => { + it('returns empty report for empty entries', () => { + const report = generateReport([]); + expect(report.entryCount).toBe(0); + expect(report.findings).toHaveLength(0); + expect(report.recommendations).toHaveLength(0); + expect(report.riskScore.grade).toBe('A'); + }); + + it('includes all required fields', () => { + const report = generateReport(makeEntries(5)); + expect(report).toHaveProperty('timestamp'); + expect(report).toHaveProperty('entryCount'); + expect(report).toHaveProperty('riskScore'); + expect(report).toHaveProperty('findings'); + expect(report).toHaveProperty('summary'); + expect(report).toHaveProperty('recommendations'); + expect(report).toHaveProperty('entryStats'); + expect(report).toHaveProperty('timeRange'); + }); + + it('sorts findings by severity (highest first)', () => { + const start = Date.now(); + const entries = []; + // Create entries that will trigger multiple finding types + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + severity: 'info', + outcome: 'success', + timestamp: new Date(start + i * 1000).toISOString(), + })); + } + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + severity: 'critical', + outcome: 'failure', + timestamp: new Date(start + 5000 + i * 1000).toISOString(), + })); + } + const report = generateReport(entries); + for (let i = 1; i < report.findings.length; i++) { + const sevOrder = { critical: 4, high: 3, medium: 2, low: 1, info: 0 }; + const prev = sevOrder[report.findings[i - 1].severity] ?? 0; + const curr = sevOrder[report.findings[i].severity] ?? 0; + expect(prev).toBeGreaterThanOrEqual(curr); + } + }); + + it('aggregates findings by type and severity', () => { + const start = Date.now(); + const entries = []; + // Create many off-hours entries + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + timestamp: new Date(new Date('2026-01-01T23:00:00Z').getTime() + i * 60_000).toISOString(), + })); + } + const report = generateReport(entries); + expect(report.summary).toHaveProperty('totalFindings'); + expect(report.summary).toHaveProperty('byType'); + expect(report.summary).toHaveProperty('bySeverity'); + }); + + it('includes entry-level stats', () => { + const entries = [ + makeEntry({ severity: 'high', category: 'auth', outcome: 'failure' }), + makeEntry({ severity: 'info', category: 'system', outcome: 'success' }), + ]; + const report = generateReport(entries); + expect(report.entryStats.bySeverity.high).toBe(1); + expect(report.entryStats.byCategory.auth).toBe(1); + expect(report.entryStats.byOutcome.failure).toBe(1); + }); + + it('provides recommendations based on findings', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = []; + // Create repeated failures + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + outcome: 'failure', + action: 'auth.login', + actor: 'attacker', + timestamp: new Date(start + i * 5000).toISOString(), + })); + } + const report = generateReport(entries); + expect(report.recommendations.length).toBeGreaterThan(0); + }); +}); + +// ─── createStreamingEngine ────────────────────────────────────────────────── + +describe('createStreamingEngine', () => { + let engine; + + beforeEach(() => { + engine = createStreamingEngine(); + }); + + it('starts with empty buffer', () => { + expect(engine.buffer).toHaveLength(0); + }); + + it('adds entries to buffer', () => { + engine.addEntry(makeEntry()); + engine.addEntry(makeEntry()); + expect(engine.buffer).toHaveLength(2); + }); + + it('returns cached findings until buffer changes', () => { + engine.addEntry(makeEntry()); + const f1 = engine.getFindings(); + const f2 = engine.getFindings(); + expect(f1).toBe(f2); // Same reference (cached) + }); + + it('invalidates cache when new entry is added', () => { + engine.addEntry(makeEntry()); + const f1 = engine.getFindings(); + engine.addEntry(makeEntry()); + const f2 = engine.getFindings(); + expect(f1).not.toBe(f2); // Different reference (cache invalidated) + }); + + it('returns valid risk score', () => { + engine.addEntry(makeEntry({ severity: 'info', outcome: 'success' })); + const risk = engine.getRiskScore(); + expect(risk).toHaveProperty('score'); + expect(risk).toHaveProperty('grade'); + expect(risk.score).toBeGreaterThanOrEqual(0); + expect(risk.score).toBeLessThanOrEqual(100); + }); + + it('returns valid report', () => { + engine.addEntry(makeEntry()); + const report = engine.getReport(); + expect(report).toHaveProperty('entryCount', 1); + expect(report).toHaveProperty('findings'); + expect(report).toHaveProperty('riskScore'); + }); + + it('reset clears the buffer', () => { + engine.addEntry(makeEntry()); + engine.addEntry(makeEntry()); + engine.reset(); + expect(engine.buffer).toHaveLength(0); + }); + + it('caps buffer at 2000 entries', () => { + for (let i = 0; i < 2100; i++) { + engine.addEntry(makeEntry()); + } + expect(engine.buffer.length).toBeLessThanOrEqual(2000); + }); + + it('analyzes entries in real-time as they arrive', () => { + const start = Date.now(); + // Add entries that create a repeated failure pattern + for (let i = 0; i < 5; i++) { + engine.addEntry(makeEntry({ + outcome: 'failure', + action: 'tx.submit', + actor: 'user-1', + timestamp: new Date(start + i * 3000).toISOString(), + })); + } + const report = engine.getReport(); + expect(report.summary.totalFindings).toBeGreaterThanOrEqual(1); + }); +}); + +// ─── Integration: full pipeline ───────────────────────────────────────────── + +describe('full analysis pipeline', () => { + it('generates a comprehensive report from realistic audit data', () => { + const start = new Date('2026-01-01T10:00:00Z').getTime(); + const entries = []; + + // Normal activity + for (let i = 0; i < 20; i++) { + entries.push(makeEntry({ + timestamp: new Date(start + i * 30_000).toISOString(), + severity: 'info', + outcome: 'success', + category: i % 3 === 0 ? 'auth' : 'transaction', + actor: `user-${i % 5}`, + })); + } + + // Off-hours activity + entries.push(makeEntry({ + timestamp: '2026-01-01T23:30:00Z', + severity: 'high', + outcome: 'failure', + category: 'security', + actor: 'unknown', + })); + + // Repeated failures + for (let i = 0; i < 5; i++) { + entries.push(makeEntry({ + timestamp: new Date(start + 1_000_000 + i * 5000).toISOString(), + severity: 'medium', + outcome: 'failure', + action: 'auth.login', + actor: 'brute-forcer', + })); + } + + const report = generateReport(entries); + expect(report.entryCount).toBe(entries.length); + expect(report.findings.length).toBeGreaterThan(0); + expect(report.riskScore.score).toBeGreaterThanOrEqual(0); + expect(report.recommendations.length).toBeGreaterThan(0); + expect(report.timeRange).toBeDefined(); + }); +});