Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions public/app/fn-app/create-mfe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare let __webpack_public_path__: string;
window.__grafana_public_path__ =
__webpack_public_path__.substring(0, __webpack_public_path__.lastIndexOf('build/')) || __webpack_public_path__;

import { cache as emotionCssCache } from '@emotion/css';
import { isNull, merge, noop, pick } from 'lodash';
import React, { ComponentType } from 'react';
import { createRoot } from 'react-dom/client';
Expand All @@ -28,7 +29,7 @@ import {
updateMfeMode,
} from 'app/store/configureMfeStore';

import { FNDashboardProps, FailedToMountGrafanaErrorName } from './types';
import { FNDashboardProps, FailedToMountGrafanaErrorName, MfeContainer } from './types';

/**
* NOTE:
Expand Down Expand Up @@ -59,6 +60,7 @@ type DeepPartial<T> = {

class createMfe {
private static readonly containerSelector = '#grafanaRoot';
private static themeStyleRoot: MfeContainer = document;
private static logger = FnLoggerService;

mode: FNDashboardProps['mode'];
Expand Down Expand Up @@ -101,6 +103,33 @@ class createMfe {
return stylesheetLink;
}

private static setThemeStyleRoot(container: FNDashboardProps['container']) {
createMfe.themeStyleRoot = container || document;
}

private static getThemeStyleRoot() {
return createMfe.themeStyleRoot;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private static getThemeLinkTarget(styleRoot: MfeContainer) {
return styleRoot instanceof Document ? styleRoot.body : styleRoot;
}

private static getThemeLinks(styleRoot: MfeContainer) {
return Array.from(styleRoot.querySelectorAll('link'));
}

private static moveEmotionStylesToStyleRoot(styleRoot = createMfe.getThemeStyleRoot()) {
const styleTarget = createMfe.getThemeLinkTarget(styleRoot);

emotionCssCache.sheet.container = styleTarget;
emotionCssCache.sheet.tags.forEach((tag) => {
if (tag.parentNode !== styleTarget) {
styleTarget.appendChild(tag);
}
});
}

private static createGrafanaTheme2(mode: FNDashboardProps['mode']) {
config.theme2 = createTheme({
colors: {
Expand Down Expand Up @@ -135,8 +164,12 @@ class createMfe {
}

// NOTE: based on grafana function: 'toggleTheme'
private static removeThemeLinks(modeToBeTurnedOff: GrafanaThemeType.Light | GrafanaThemeType.Dark, timeout?: number) {
Array.from(document.getElementsByTagName('link')).forEach(createMfe.removeThemeLink(modeToBeTurnedOff, timeout));
private static removeThemeLinks(
modeToBeTurnedOff: GrafanaThemeType.Light | GrafanaThemeType.Dark,
styleRoot: MfeContainer,
timeout?: number
) {
createMfe.getThemeLinks(styleRoot).forEach(createMfe.removeThemeLink(modeToBeTurnedOff, timeout));
}

private static removeThemeLink(modeToBeTurnedOff: FNDashboardProps['mode'], timeout?: number) {
Expand All @@ -162,7 +195,11 @@ class createMfe {
* NOTE:
* If isRuntimeOnly then the stylesheets of the turned off theme are not removed
*/
private static loadFnTheme = (mode: FNDashboardProps['mode'] = GrafanaThemeType.Light, isRuntimeOnly = false) => {
private static loadFnTheme = (
mode: FNDashboardProps['mode'] = GrafanaThemeType.Light,
isRuntimeOnly = false,
styleRoot?: MfeContainer
) => {
createMfe.logger.info('Trying to load theme.', { mode });

const grafanaTheme2 = createMfe.createGrafanaTheme2(mode);
Expand All @@ -179,11 +216,13 @@ class createMfe {
return;
}

createMfe.removeThemeLinks(createMfe.toggleTheme(mode));
const themeStyleRoot = styleRoot ?? createMfe.getThemeStyleRoot();

createMfe.removeThemeLinks(createMfe.toggleTheme(mode), themeStyleRoot);

const newCssLink = createMfe.styleSheetLink;
newCssLink.href = config.bootData.themePaths[mode];
document.body.appendChild(newCssLink);
createMfe.getThemeLinkTarget(themeStyleRoot).appendChild(newCssLink);

createMfe.logger.info('Successfully loaded theme.', { mode });
};
Expand All @@ -198,6 +237,8 @@ class createMfe {
const lifeCycleFn: FrameworkLifeCycles['mount'] = (props: FNDashboardProps) => {
return new Promise((res, rej) => {
try {
createMfe.setThemeStyleRoot(props.container);
createMfe.moveEmotionStylesToStyleRoot();
createMfe.loadFnTheme(props.mode);
createMfe.Component = Component;

Expand Down Expand Up @@ -250,6 +291,7 @@ class createMfe {
}

backendSrv.cancelAllInFlightRequests();
createMfe.setThemeStyleRoot(null);

return Promise.resolve(!!container);
};
Expand All @@ -260,10 +302,14 @@ class createMfe {
static updateFnApp() {
const lifeCycleFn: FrameworkLifeCycles['update'] = ({
mode,
container,
...other
}: FNDashboardProps & {
readonly renderingDashboardUid?: string;
}) => {
createMfe.setThemeStyleRoot(container ?? createMfe.getThemeStyleRoot());
createMfe.moveEmotionStylesToStyleRoot();

if (mode && mfeGetStoreState().fnGlobalReducer.mode !== mode) {
mfeDispatch(updateMfeMode(mode));

Expand Down
57 changes: 42 additions & 15 deletions public/app/fn-app/fn-app-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useState, useEffect, FC, PropsWithChildren } from 'react';
import createCache from '@emotion/cache';
import { cache as emotionCssCache } from '@emotion/css';
import { CacheProvider } from '@emotion/react';
import { useState, useEffect, FC, PropsWithChildren, useMemo } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { Store } from 'redux';
Expand All @@ -18,11 +21,32 @@ import app from '../fn_app';
import { FNDashboardProps } from './types';

type FnAppProviderProps = Pick<FNDashboardProps, 'fnError'> & {
container?: FNDashboardProps['container'];
store: Store<StoreState>;
};

export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props) => {
const { children } = props;
const emotionCache = useMemo(
() => {
const container =
props.container instanceof Document ? props.container.head : props.container || document.head;

emotionCssCache.sheet.container = container;
emotionCssCache.sheet.tags.forEach((tag) => {
if (tag.parentNode !== container) {
container.appendChild(tag);
}
});

return createCache({
key: 'grafana-mf',
container,
});
},
[props.container]
);
Comment on lines +30 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Mutating the shared global Emotion cache inside useMemo breaks multi-dashboard rendering.

emotionCssCache is the process-wide singleton from @emotion/css shared by the host app and every dashboard. DashboardPortal renders one FnAppProvider per dashboard, each with its own shadow-root container. Two problems:

  1. Each instance reassigns emotionCssCache.sheet.container and appendChild's all existing @emotion/css tags into its own shadow root. With more than one dashboard the last render wins, relocating those style tags out of the host (and out of every other dashboard's root), so other consumers lose their styles.
  2. These are side effects (global mutation + DOM moves) executed inside useMemo, which must be pure. Under concurrent rendering React may invoke or discard the memo callback speculatively, making the relocation unreliable.

Keep createCache in useMemo for the per-container scoping, but move the global-sheet relocation into an effect, and reconsider whether the shared @emotion/css cache should be re-parented at all when multiple shadow roots are active.

♻️ Suggested direction
-  const emotionCache = useMemo(
-    () => {
-      const container =
-        props.container instanceof Document ? props.container.head : props.container || document.head;
-
-      emotionCssCache.sheet.container = container;
-      emotionCssCache.sheet.tags.forEach((tag) => {
-        if (tag.parentNode !== container) {
-          container.appendChild(tag);
-        }
-      });
-
-      return createCache({
-        key: 'grafana-mf',
-        container,
-      });
-    },
-    [props.container]
-  );
+  const container = useMemo(
+    () => (props.container instanceof Document ? props.container.head : props.container || document.head),
+    [props.container]
+  );
+  const emotionCache = useMemo(() => createCache({ key: 'grafana-mf', container }), [container]);
+
+  useEffect(() => {
+    // Re-parent existing `@emotion/css` tags into the scoped container.
+    // NOTE: this still touches a shared global cache; verify it is safe when
+    // multiple dashboards (shadow roots) are mounted simultaneously.
+    emotionCssCache.sheet.container = container;
+    emotionCssCache.sheet.tags.forEach((tag) => {
+      if (tag.parentNode !== container) {
+        container.appendChild(tag);
+      }
+    });
+  }, [container]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const emotionCache = useMemo(
() => {
const container =
props.container instanceof Document ? props.container.head : props.container || document.head;
emotionCssCache.sheet.container = container;
emotionCssCache.sheet.tags.forEach((tag) => {
if (tag.parentNode !== container) {
container.appendChild(tag);
}
});
return createCache({
key: 'grafana-mf',
container,
});
},
[props.container]
);
const container = useMemo(
() => (props.container instanceof Document ? props.container.head : props.container || document.head),
[props.container]
);
const emotionCache = useMemo(() => createCache({ key: 'grafana-mf', container }), [container]);
useEffect(() => {
// Re-parent existing `@emotion/css` tags into the scoped container.
// NOTE: this still touches a shared global cache; verify it is safe when
// multiple dashboards (shadow roots) are mounted simultaneously.
emotionCssCache.sheet.container = container;
emotionCssCache.sheet.tags.forEach((tag) => {
if (tag.parentNode !== container) {
container.appendChild(tag);
}
});
}, [container]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/app/fn-app/fn-app-provider.tsx` around lines 30 - 48, The code
currently mutates the process-wide emotionCssCache and moves its tags inside the
useMemo callback for the FnAppProvider, which breaks multi-dashboard rendering;
keep the per-container createCache call inside useMemo (createCache with key
'grafana-mf' and container) but remove any reassignment of
emotionCssCache.sheet.container and DOM appendChild calls from the memo
callback, and instead perform any required global-sheet relocation in a
useEffect (or avoid relocating shared `@emotion/css` entirely) so side effects run
only after mount; reference emotionCssCache, useMemo, createCache, FnAppProvider
and DashboardPortal when making the change.

const themeHref = config.bootData.themePaths[config.theme2.colors.mode];

const [ready, setReady] = useState(false);
navigationLogger('AppWrapper', false, 'rendering');
Expand All @@ -40,19 +64,22 @@ export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props)
}

return (
<Provider store={props.store}>
<BrowserRouter>
<ErrorBoundaryAlert style="page">
<GrafanaContext.Provider value={app.context}>
<ThemeProvider value={config.theme2}>
<div data-grafana-mf-root>
<GlobalStyles prefix="[data-grafana-mf-root]" />
{children}
</div>
</ThemeProvider>
</GrafanaContext.Provider>
</ErrorBoundaryAlert>
</BrowserRouter>
</Provider>
<CacheProvider value={emotionCache}>
<link rel="stylesheet" href={themeHref} />
<Provider store={props.store}>
<BrowserRouter>
<ErrorBoundaryAlert style="page">
<GrafanaContext.Provider value={app.context}>
<ThemeProvider value={config.theme2}>
<div data-grafana-mf-root>
<GlobalStyles prefix="[data-grafana-mf-root]" />
{children}
</div>
</ThemeProvider>
</GrafanaContext.Provider>
</ErrorBoundaryAlert>
</BrowserRouter>
</Provider>
</CacheProvider>
);
};
26 changes: 14 additions & 12 deletions public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,20 @@ export const DashboardPortal: FC<FNDashboardComponentProps> = (p) => {

return (
<RenderPortal ID={props.portalContainerID} key={uid}>
<FnAppProvider fnError={p.fnError} store={store}>
<div className="page-dashboard">
<RenderFNDashboard
{...{
...props,
...p,
uid,
mode: globalFnProps.mode,
}}
/>
</div>
</FnAppProvider>
{(container) => (
<FnAppProvider container={container} fnError={p.fnError} store={store}>
<div className="page-dashboard">
<RenderFNDashboard
{...{
...props,
...p,
uid,
mode: globalFnProps.mode,
}}
/>
</div>
</FnAppProvider>
)}
</RenderPortal>
);
});
Expand Down
5 changes: 4 additions & 1 deletion public/app/fn-app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ export type GrafanaMicroFrontendActions = {
export type AnyObject<K extends string | number | symbol = string, V = any> = {
[key in K]: V;
};

export type MfeContainer = Document | DocumentFragment | HTMLElement;

export interface FNDashboardProps extends FnState {
name: string;
fnError?: ReactNode;
isLoading: (isLoading: boolean) => void;
setErrors: (errors?: { [K: number | string]: string }) => void;
container?: HTMLElement | null;
container?: MfeContainer | null;
mode: FnGlobalState['mode'];
}
14 changes: 10 additions & 4 deletions public/app/fn-app/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { Portal } from '@mui/material';
import { FC, PropsWithChildren } from 'react';
import { FC, ReactNode } from 'react';
import { createPortal } from 'react-dom';

import { MfeContainer } from './types';

export interface RenderPortalProps {
ID: string;
children: ReactNode | ((container: MfeContainer) => ReactNode);
}

export const RenderPortal: FC<PropsWithChildren<RenderPortalProps>> = ({ ID, children }) => {
export const RenderPortal: FC<RenderPortalProps> = ({ ID, children }) => {
const container = document.getElementById(ID);

if (!container) {
return null;
}

return <Portal container={container}>{children}</Portal>;
const shadowRoot = container.shadowRoot || container.attachShadow({ mode: 'open' });
const content = typeof children === 'function' ? children(shadowRoot) : children;

return createPortal(content, shadowRoot);
};
27 changes: 23 additions & 4 deletions public/microfrontends/fn_dashboard/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
<!doctype html><html lang="en"><head><title>CodeRabbit Micro-frontend</title><base href="/"/></head><body class="theme-light app-grafana"><div id="grafanaRoot"></div><script nonce="">window.fnData = {
<!doctype html>
<html lang="en">
<head>
<title>CodeRabbit Micro-frontend</title>
<base href="/" />
</head>

<body class="theme-light app-grafana">
<div id="grafanaRoot"></div>
<script nonce="">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Empty nonce attribute defeats CSP protection.

The nonce="" attribute on this inline script tag has an empty value, which defeats Content Security Policy (CSP) protection. If CSP is enabled with script-src 'nonce-{value}', an empty nonce effectively allows any inline script to execute, creating a security vulnerability.

Either populate the nonce with a cryptographically random value generated server-side, or remove the attribute if CSP is not being used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/microfrontends/fn_dashboard/index.html` at line 10, The inline
<script> tag currently has an empty nonce attribute (nonce="") which nullifies
CSP protection; either remove the nonce attribute entirely if you are not using
CSP, or populate it with a server-generated cryptographically secure nonce value
and inject that same value into the server's Content-Security-Policy header
(matching 'nonce-{value}'); locate the inline script tag with nonce="" in the
HTML (the <script nonce=""> element) and implement server-side
generation/injection of the nonce or remove the attribute accordingly so the CSP
remains effective.

window.fnData = {
themePaths: {
light: '../../../public/build/grafana.light.d120ca3c6b86113973bc.css',
dark: '../../../public/build/grafana.dark.f4571afb90da68f52521.css',
light: '../../../public/build/grafana.light.65904cef23d9f7f76c4a.css',
dark: '../../../public/build/grafana.dark.7c4d93ecbdd92098eb9a.css',
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if the hashed CSS files referenced in the HTML exist

# Expected CSS files based on the HTML
LIGHT_CSS="public/build/grafana.light.65904cef23d9f7f76c4a.css"
DARK_CSS="public/build/grafana.dark.7c4d93ecbdd92098eb9a.css"

echo "Checking for CSS files..."
if [ -f "$LIGHT_CSS" ]; then
  echo "✓ Found: $LIGHT_CSS"
else
  echo "✗ Missing: $LIGHT_CSS"
fi

if [ -f "$DARK_CSS" ]; then
  echo "✓ Found: $DARK_CSS"
else
  echo "✗ Missing: $DARK_CSS"
fi

# Also search for any grafana theme CSS files to see what actually exists
echo -e "\nAll Grafana theme CSS files in public/build/:"
fd -e css 'grafana\.(light|dark)\.' public/build/ --exec echo "{}"

Repository: coderabbitai/grafana

Length of output: 381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect index.html base + themePaths snippet =="
nl -ba public/microfrontends/fn_dashboard/index.html | sed -n '1,80p'

echo
echo "== Locate referenced hashed CSS files anywhere in repo =="
LIGHT_REF="grafana.light.65904cef23d9f7f76c4a.css"
DARK_REF="grafana.dark.7c4d93ecbdd92098eb9a.css"

echo "Searching for: $LIGHT_REF"
rg -n --hidden --no-ignore -S "$LIGHT_REF" . || true
echo "Searching for: $DARK_REF"
rg -n --hidden --no-ignore -S "$DARK_REF" . || true

echo
echo "== List any grafana.light.* and grafana.dark.* CSS files in repo =="
fd -e css "grafana\.(light|dark)\..*" . || true

echo
echo "== Check for common build output dirs (existence only) =="
for d in public/build build public/dist public/assets dist assets; do
  if [ -d "$d" ]; then echo "Found dir: $d"; else echo "Missing dir: $d"; fi
done

Repository: coderabbitai/grafana

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect index.html base + themePaths snippet =="
awk '{printf "%5d  %s\n", NR, $0}' public/microfrontends/fn_dashboard/index.html | sed -n '1,120p'

echo
echo "== Locate referenced hashed CSS files anywhere in repo =="
LIGHT_REF="grafana.light.65904cef23d9f7f76c4a.css"
DARK_REF="grafana.dark.7c4d93ecbdd92098eb9a.css"

echo "Searching for: $LIGHT_REF"
rg -n --hidden --no-ignore -S "$LIGHT_REF" . || true
echo "Searching for: $DARK_REF"
rg -n --hidden --no-ignore -S "$DARK_REF" . || true

echo
echo "== List any grafana.light.* and grafana.dark.* CSS files in repo =="
fd -e css "grafana\.(light|dark)\..*" . || true

echo
echo "== Check for common build output dirs (existence only) =="
for d in public/build public/dist public/assets build dist assets; do
  if [ -d "$d" ]; then echo "Found dir: $d"; else echo "Missing dir: $d"; fi
done

Repository: coderabbitai/grafana

Length of output: 1680


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check whether referenced build JS files exist =="
RUNTIME="public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js"
BUNDLE="public/build/fn_dashboard.61924fc7a748b4f2ecc3.js"

for f in "$RUNTIME" "$BUNDLE"; do
  if [ -f "$f" ]; then
    echo "✓ Found: $f"
  else
    echo "✗ Missing: $f"
  fi
done

Repository: coderabbitai/grafana

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --hidden --no-ignore -S "runtime~fn_dashboard\.a5d060ef4f8cb38ef003\.js" . || true
rg -n --hidden --no-ignore -S "fn_dashboard\.61924fc7a748b4f2ecc3\.js" . || true
rg -n --hidden --no-ignore -S "grafana\.(light|dark)\." public/microfrontends/fn_dashboard/index.html || true
ls -la public || true
ls -la public/microfrontends/fn_dashboard || true

Repository: coderabbitai/grafana

Length of output: 2017


Fix asset URLs: referenced hashed CSS/JS under public/build/ are missing in this repo snapshot

public/microfrontends/fn_dashboard/index.html resolves (with <base href="/" />) to /public/build/grafana.{light|dark}.<hash>.css and also loads /public/build/runtime~fn_dashboard.<hash>.js + /public/build/fn_dashboard.<hash>.js, but public/build/ (and those hashed files) aren’t present here—these requests will 404 unless your build/deploy step generates/copies them into public/build (or the template should be updated to the real asset locations).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/microfrontends/fn_dashboard/index.html` around lines 13 - 14, The
template references hashed assets under public/build (e.g. the strings
'public/build/grafana.light.<hash>.css', 'public/build/grafana.dark.<hash>.css'
and runtime~fn_dashboard.<hash>.js / fn_dashboard.<hash>.js) which are missing;
either update the asset URLs in the index.html to the actual deployed locations
(e.g. remove the leading "public/" so they resolve to /build/...) or make the
build/deploy process copy/generate those hashed files into public/build; locate
the asset declarations in the fn_dashboard index.html (the base href and the
light/dark CSS and JS script references) and change them to point to the correct
runtime paths or to template placeholders that your bundler will replace during
build.

}
};</script><script nonce="" src="../../../public/build/runtime.6bc90f2808f6a6cec604.js" integrity="../../../sha384-3As+cCSgxk8iweHxuF0ngRBgr9/ctzbHdj+SGyxLEsEcj6HkIQ9CSdeyXHvbZWHz" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/6029.5c0893d1623856e7b325.js" integrity="../../../sha384-LCMBgpyynxT2jXlhS9LdLRKvKFwONDIYBW6yP4ZgNSZo7VxpgMGsUdspsX7oCvt5" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/5234.b70fab3b1fc48a116b38.js" integrity="../../../sha384-6z/x7G0Kuq5vHq8SixVqItA9g2cqD1Tw4nVd8f947akcoV/Y/8suL1cymdz83GDW" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/4159.2474818cd0a590b4b852.js" integrity="../../../sha384-mDVs9x2bqCV4KOdzAVdPIhNhftT6jp5rgT4z0W2PFICSgYO9WspVQ1NZEZ9uopBj" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/2410.606a87b065106e81ad31.js" integrity="../../../sha384-BFVRm68sCi1AfvPOncSziFN0wyR9pFWDILKADQNXW3osulVTaKUFvv6PVohbLlgE" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/9569.5956dfdd4b3fdd6a93a6.js" integrity="../../../sha384-Rt0UcbPtFvyQunoSipLsiTe0QFnqPd8+C9P4dKme0l0oWhfMRdjHufKTF4GjR4GN" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/6263.c2ee3ac2fe2934607fe0.js" integrity="../../../sha384-2VdfTyu5FomdOEQn4OvRVDRX1C8TWJAVA9tPJlRoPq0FvWM+KhrzCtq6HNg2m9Fe" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/6947.6fa9ff70b916365b6259.js" integrity="../../../sha384-IbuK21HpudeblOqkfNJfp6wvrHVlhxcd2s830P75awAlEiCS5pLngZ18qg0S0K/W" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/4894.89be4a92855ef9eb683b.js" integrity="../../../sha384-EoWaAzRvJN8kNdv/uj04uulL4FoocvQgg0+KIIlTi+FoNCAZHcxAj/pJ57FZi3qK" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/530.1c5ae442b33495c705a5.js" integrity="../../../sha384-Cv4HLe8qompnosQrBeW5214V6jyT5fWyYce0wl/H6F0e0VdI+icvpGcLUrnCqnHs" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/7223.d145fd99105c51a27905.js" integrity="../../../sha384-Ps7r+bS4dMRkHGzfn2hbwrLuPvsBrvH0BxwjA44TkTUo2u4JVwTRyvlT1EV+XtyH" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/280.04d501de6deac449c563.js" integrity="../../../sha384-ET/a2+qGxnfC4uCd8wYZrOjvnkYk7ZgeNwly0P/UoiogdLNhIftTGwFXRLqKEh5O" crossorigin="anonymous"></script><script nonce="" src="../../../public/build/fn_dashboard.3b27ec7a6c915d13b983.js" integrity="../../../sha384-y5RXL2jFR/Tl6RXV1RQMqZI3kbEq6s8y6SZGjZvxKC9I2eQAbvbRrV20qIZlS8Mg" crossorigin="anonymous"></script></body></html>
};
</script>


<script nonce="" src="../../../public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js" type="text/javascript"></script>

<script nonce="" src="../../../public/build/fn_dashboard.61924fc7a748b4f2ecc3.js" type="text/javascript"></script>
Comment on lines +20 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Empty nonce attributes and missing integrity protection.

These script tags have two security issues:

  1. Empty nonce attributes (nonce=""): Same CSP bypass vulnerability as noted earlier. Each script with an empty nonce defeats Content Security Policy protection.

  2. Missing Subresource Integrity (SRI): The AI summary confirms that integrity and crossorigin attributes were removed. This is a security regression that eliminates protection against script tampering. SRI ensures that loaded scripts haven't been modified on the CDN or during transit.

Recommended fixes:

  • Populate nonce attributes with server-generated cryptographically random values, or remove if CSP is not used
  • Restore integrity attributes with SHA-384/SHA-512 hashes of the script files
  • Add crossorigin="anonymous" to enable SRI with CORS
🔒 Example with SRI attributes
-    <script nonce="" src="../../../public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js" type="text/javascript"></script>
+    <script nonce="{SERVER_GENERATED_NONCE}" src="../../../public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js" type="text/javascript" integrity="sha384-{HASH}" crossorigin="anonymous"></script>
       
-    <script nonce="" src="../../../public/build/fn_dashboard.61924fc7a748b4f2ecc3.js" type="text/javascript"></script>
+    <script nonce="{SERVER_GENERATED_NONCE}" src="../../../public/build/fn_dashboard.61924fc7a748b4f2ecc3.js" type="text/javascript" integrity="sha384-{HASH}" crossorigin="anonymous"></script>

Note: Generate integrity hashes using: openssl dgst -sha384 -binary <file> | openssl base64 -A

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/microfrontends/fn_dashboard/index.html` around lines 20 - 22, The two
script tags referencing runtime~fn_dashboard.a5d060ef4f8cb38ef003.js and
fn_dashboard.61924fc7a748b4f2ecc3.js currently have empty nonce attributes and
no SRI/CORS attributes; fix by removing empty nonce="" or (preferably) replacing
with a server-generated cryptographically-random nonce value that matches the
page CSP, and restore Subresource Integrity by computing and adding
integrity="sha384-..." (or sha512) for each script file and add
crossorigin="anonymous" so SRI can function; locate the tags in
public/microfrontends/fn_dashboard/index.html and update the script elements
referencing those two filenames accordingly.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if the hashed JS bundles referenced in the HTML exist

# Expected JS files based on the HTML
RUNTIME_JS="public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js"
MAIN_JS="public/build/fn_dashboard.61924fc7a748b4f2ecc3.js"

echo "Checking for JS bundles..."
if [ -f "$RUNTIME_JS" ]; then
  echo "✓ Found: $RUNTIME_JS"
else
  echo "✗ Missing: $RUNTIME_JS"
fi

if [ -f "$MAIN_JS" ]; then
  echo "✓ Found: $MAIN_JS"
else
  echo "✗ Missing: $MAIN_JS"
fi

# Also search for any fn_dashboard JS files to see what actually exists
echo -e "\nAll fn_dashboard JS files in public/build/:"
fd -e js 'fn_dashboard' public/build/ --exec echo "{}"

Repository: coderabbitai/grafana

Length of output: 385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the expected hashed bundle filenames anywhere in the repo
echo "Searching for exact hashed bundle filenames..."
rg -n --hidden --no-ignore-vcs -S "runtime~fn_dashboard\.a5d060ef4f8cb38ef003\.js|fn_dashboard\.61924fc7a748b4f2ecc3\.js" . || true

echo
echo "Finding files by name (may take a moment)..."
find . -type f \( -name 'runtime~fn_dashboard.a5d060ef4f8cb38ef003.js' -o -name 'fn_dashboard.61924fc7a748b4f2ecc3.js' \) -print || true

echo
# 2) Check whether a public/build directory exists anywhere under repo
echo "Checking for build output directories..."
find . -type d -path "*/public/build" -print || true
find . -type d -name build -maxdepth 4 -print || true

echo
# 3) Show the relevant section of the HTML for base href and script tags
echo "---- public/microfrontends/fn_dashboard/index.html (relevant section) ----"
nl -ba public/microfrontends/fn_dashboard/index.html | sed -n '1,120p'

Repository: coderabbitai/grafana

Length of output: 713


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Does any fn_dashboard JS exist anywhere?"
find . -type f \( -name '*fn_dashboard*js' -o -name '*runtime~fn_dashboard*js' \) -print || true

echo
echo "2) What JS files exist under pkg/build and scripts/build (limited by fn_dashboard keywords)?"
for d in ./pkg/build ./scripts/build; do
  if [ -d "$d" ]; then
    echo "-- $d --"
    find "$d" -type f -name '*fn_dashboard*js' -print || true
    find "$d" -type f -name '*runtime*fn_dashboard*js' -print || true
  fi
done

echo
echo "3) Reprint public/microfrontends/fn_dashboard/index.html lines 1-60 (no nl)."
sed -n '1,80p' public/microfrontends/fn_dashboard/index.html

Repository: coderabbitai/grafana

Length of output: 1027


Fix: referenced hashed fn_dashboard JS bundles aren’t present at /public/build

  • In public/microfrontends/fn_dashboard/index.html (lines 20-22), the page loads:
    • ../../../public/build/runtime~fn_dashboard.a5d060ef4f8cb38ef003.js
    • ../../../public/build/fn_dashboard.61924fc7a748b4f2ecc3.js
  • The repo checkout has no public/build directory and these exact hashed JS files don’t exist anywhere in the repository, so the script tags will fail (404) unless your build/deploy pipeline generates/serves them at /public/build.
  • Update the hashes/paths to match the actual build outputs or ensure the bundles are produced and available at the expected location at runtime.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/microfrontends/fn_dashboard/index.html` around lines 20 - 22, In
public/microfrontends/fn_dashboard/index.html the script tags reference
non-existent hashed bundles runtime~fn_dashboard.a5d060ef4f8cb38ef003.js and
fn_dashboard.61924fc7a748b4f2ecc3.js; either update those src attributes to
point to the actual built filenames/paths produced by your bundler (replace the
two referenced filenames with the real outputs) or ensure your build/pipeline
emits those specific files into the public/build location so the paths resolve
at runtime (adjust your build config or deployment static asset step to
produce/serve the expected bundles).


</body>
</html>
Loading