@backstage/plugin-techdocs-node vulnerable to possible Path Traversal in TechDocs Local Generator
Description
Backstage is an open framework for building developer portals, and @backstage/plugin-techdocs-node provides common node.js functionalities for TechDocs. In versions of @backstage/plugin-techdocs-node prior to 1.13.11 and 1.14.1, a path traversal vulnerability in the TechDocs local generator allows attackers to read arbitrary files from the host filesystem when Backstage is configured with techdocs.generator.runIn: local. When processing documentation from untrusted sources, symlinks within the docs directory are followed by MkDocs during the build process. File contents are embedded into generated HTML and exposed to users who can view the documentation. This vulnerability is fixed in @backstage/plugin-techdocs-node versions 1.13.11 and 1.14.1. Some workarounds are available. Switch to runIn: docker in app-config.yaml and/or restrict write access to TechDocs source repositories to trusted users only.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@backstage/plugin-techdocs-nodenpm | >= 1.14.0, < 1.14.1 | 1.14.1 |
@backstage/plugin-techdocs-nodenpm | < 1.13.11 | 1.13.11 |
Affected products
1Patches
108f388e3394bMerge pull request #31055 from secustor/feat/add-devtools-extension
28 files changed · +641 −34
.changeset/slimy-ghosts-rescue.md+9 −0 added@@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-devtools-react': minor +'@backstage/plugin-devtools': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-devtools-common': patch +--- + +Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system
packages/app-next/app-config.yaml+5 −0 modified@@ -114,6 +114,11 @@ app: # - entity-content:azure-devops/pull-requests # - entity-content:azure-devops/git-tags + # Enable the catalog-unprocessed-entities tab in devtools + - devtools-content:catalog-unprocessed-entities: true + # Disable the catalog-unprocessed-entities element outside devtools + - page:catalog-unprocessed-entities: false + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe',
packages/app-next/package.json+1 −0 modified@@ -57,6 +57,7 @@ "@backstage/plugin-catalog-import": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", "@backstage/plugin-home": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^",
packages/app-next/src/App.tsx+9 −0 modified@@ -45,6 +45,8 @@ import { convertLegacyPageExtension } from '@backstage/core-compat-api'; import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; import { pluginInfoResolver } from './pluginInfoResolver'; import { appModuleNav } from './modules/appModuleNav'; +import devtoolsPlugin from '@backstage/plugin-devtools/alpha'; +import { unprocessedEntitiesDevToolsContent } from '@backstage/plugin-catalog-unprocessed-entities/alpha'; /* @@ -116,6 +118,11 @@ const notFoundErrorPageModule = createFrontendModule({ extensions: [notFoundErrorPage], }); +const devtoolsPluginUnprocessed = createFrontendModule({ + pluginId: 'catalog-unprocessed-entities', + extensions: [unprocessedEntitiesDevToolsContent], +}); + const collectedLegacyPlugins = convertLegacyAppRoot( <FlatRoutes> <Route path="/catalog-import" element={<CatalogImportPage />} /> @@ -133,6 +140,8 @@ const app = createApp({ notFoundErrorPageModule, appModuleNav, customHomePageModule, + devtoolsPlugin, + devtoolsPluginUnprocessed, ...collectedLegacyPlugins, ], advanced: {
plugins/catalog-unprocessed-entities/package.json+2 −0 modified@@ -50,11 +50,13 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-unprocessed-entities-common": "workspace:^", + "@backstage/plugin-devtools-react": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60",
plugins/catalog-unprocessed-entities/README.md+19 −1 modified@@ -52,18 +52,36 @@ Import `catalogUnprocessedEntitiesPlugin` in your `App.tsx` and add it to your a ```typescript import catalogUnprocessedEntitiesPlugin from '@backstage/plugin-catalog-unprocessed-entities'; +import { unprocessedEntitiesDevToolsContent } from '@backstage/plugin-catalog-unprocessed-entities/alpha'; -// ... +// Optionally add unprocessed entities route to devtools +const devtoolsPluginUnprocessed = createFrontendModule({ + pluginId: 'catalog-unprocessed-entities', + extensions: [unprocessedEntitiesDevToolsContent], +}); export const app = createApp({ features: [ // ... catalogUnprocessedEntitiesPlugin, + + // Optionally add unprocessed entities route to devtools + devtoolsPluginUnprocessed, + devtoolsPlugin, // devtools plugin needs to be added too, if autodiscover is disabled // ... ], }); ``` +```yaml +app: + extensions: + # Enable the catalog-unprocessed-entities tab in devtools + - devtools-content:catalog-unprocessed-entities: true + # Disable the catalog-unprocessed-entities element outside devtools including the sidebar + - page:catalog-unprocessed-entities: false +``` + ## Customization If you want to use the provided endpoints in a different way, you can use the ApiRef doing the following:
plugins/catalog-unprocessed-entities/report-alpha.api.md+28 −0 modified@@ -6,6 +6,7 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { DevToolsContentBlueprintParams } from '@backstage/plugin-devtools-react'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; @@ -89,5 +90,32 @@ const _default: OverridableFrontendPlugin< >; export default _default; +// @alpha +export const unprocessedEntitiesDevToolsContent: OverridableExtensionDefinition<{ + kind: 'devtools-content'; + name: undefined; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + output: + | ExtensionDataRef<string, 'core.routing.path', {}> + | ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}> + | ExtensionDataRef< + RouteRef_2<AnyRouteRefParams>, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef<string, 'core.title', {}>; + inputs: {}; + params: DevToolsContentBlueprintParams; +}>; + // (No @packageDocumentation comment for this package) ```
plugins/catalog-unprocessed-entities/src/alpha/devToolsContent.tsx+36 −0 added@@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DevToolsContentBlueprint } from '@backstage/plugin-devtools-react'; + +/** + * DevTools content for catalog unprocessed entities. + * + * @alpha + */ +export const unprocessedEntitiesDevToolsContent = DevToolsContentBlueprint.make( + { + disabled: true, + params: { + path: 'unprocessed-entities', + title: 'Unprocessed Entities', + loader: () => + import('../components/UnprocessedEntities').then(m => ( + <m.UnprocessedEntitiesContent /> + )), + }, + }, +);
plugins/catalog-unprocessed-entities/src/alpha/index.ts+1 −0 modified@@ -15,3 +15,4 @@ */ export { default } from './plugin'; +export { unprocessedEntitiesDevToolsContent } from './devToolsContent';
plugins/devtools-backend/package.json+2 −1 modified@@ -7,7 +7,8 @@ "pluginPackages": [ "@backstage/plugin-devtools", "@backstage/plugin-devtools-backend", - "@backstage/plugin-devtools-common" + "@backstage/plugin-devtools-common", + "@backstage/plugin-devtools-react" ] }, "publishConfig": {
plugins/devtools-common/package.json+2 −1 modified@@ -8,7 +8,8 @@ "pluginPackages": [ "@backstage/plugin-devtools", "@backstage/plugin-devtools-backend", - "@backstage/plugin-devtools-common" + "@backstage/plugin-devtools-common", + "@backstage/plugin-devtools-react" ] }, "publishConfig": {
plugins/devtools/package.json+3 −1 modified@@ -7,7 +7,8 @@ "pluginPackages": [ "@backstage/plugin-devtools", "@backstage/plugin-devtools-backend", - "@backstage/plugin-devtools-common" + "@backstage/plugin-devtools-common", + "@backstage/plugin-devtools-react" ] }, "publishConfig": { @@ -58,6 +59,7 @@ "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-devtools-common": "workspace:^", + "@backstage/plugin-devtools-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1",
plugins/devtools-react/catalog-info.yaml+10 −0 added@@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-devtools-react + title: '@backstage/plugin-devtools-react' + description: Web library for the devtools plugin +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers
plugins/devtools-react/.eslintrc.js+1 −0 added@@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
plugins/devtools-react/package.json+67 −0 added@@ -0,0 +1,67 @@ +{ + "name": "@backstage/plugin-devtools-react", + "version": "0.0.0", + "description": "Web library for the devtools plugin", + "backstage": { + "role": "web-library", + "pluginId": "devtools", + "pluginPackages": [ + "@backstage/plugin-devtools", + "@backstage/plugin-devtools-backend", + "@backstage/plugin-devtools-common", + "@backstage/plugin-devtools-react" + ] + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/devtools-react" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@material-ui/core": "^4.9.13" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } +}
plugins/devtools-react/README.md+5 −0 added@@ -0,0 +1,5 @@ +# @backstage/plugin-devtools-react + +Welcome to the web library package for the `devtools` plugin! + +_This plugin was created through the Backstage CLI_
plugins/devtools-react/report.api.md+50 −0 added@@ -0,0 +1,50 @@ +## API Report File for "@backstage/plugin-devtools-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @public +export const DevToolsContentBlueprint: ExtensionBlueprint<{ + kind: 'devtools-content'; + params: DevToolsContentBlueprintParams; + output: + | ExtensionDataRef<string, 'core.routing.path', {}> + | ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}> + | ExtensionDataRef< + RouteRef<AnyRouteRefParams>, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef<string, 'core.title', {}>; + inputs: {}; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + dataRefs: never; +}>; + +// @public +export interface DevToolsContentBlueprintParams { + // (undocumented) + loader: () => Promise<JSX_2.Element>; + // (undocumented) + path: string; + // (undocumented) + routeRef?: RouteRef; + // (undocumented) + title: string; +} +```
plugins/devtools-react/src/devToolsContentBlueprint.tsx+87 −0 added@@ -0,0 +1,87 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreExtensionData, + createExtensionBlueprint, + ExtensionBoundary, + RouteRef, +} from '@backstage/frontend-plugin-api'; +import { JSX } from 'react'; + +/** + * Parameters for creating a DevTools route extension + * @public + */ +export interface DevToolsContentBlueprintParams { + path: string; + title: string; + loader: () => Promise<JSX.Element>; + routeRef?: RouteRef; +} + +/** + * Extension blueprint for creating DevTools content pages (appearing as tabs) + * + * @example + * ```tsx + * const myDevToolsContent = DevToolsContentBlueprint.make({ + * { + * params: { + * path: 'my-dev-tools', + * title: 'My DevTools', + * loader: () => + * import('../components/MyDevTools').then(m => + * compatWrapper(<m.MyDevTools />), + * ), + * }, + * }, + * }); + * ``` + * @public + */ +export const DevToolsContentBlueprint = createExtensionBlueprint({ + kind: 'devtools-content', + attachTo: { id: 'page:devtools', input: 'contents' }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + coreExtensionData.title, + ], + config: { + schema: { + path: z => z.string().optional(), + title: z => z.string().optional(), + }, + }, + *factory(params: DevToolsContentBlueprintParams, { node, config }) { + const path = config.path ?? params.path; + const title = config.title ?? params.title; + + yield coreExtensionData.reactElement( + ExtensionBoundary.lazy(node, params.loader), + ); + + yield coreExtensionData.routePath(path); + + yield coreExtensionData.title(title); + + if (params.routeRef) { + yield coreExtensionData.routeRef(params.routeRef); + } + }, +});
plugins/devtools-react/src/index.ts+25 −0 added@@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Web library for the devtools plugin. + * + * @packageDocumentation + */ +export { + type DevToolsContentBlueprintParams, + DevToolsContentBlueprint, +} from './devToolsContentBlueprint';
plugins/devtools/README.md+33 −0 modified@@ -166,6 +166,39 @@ You can also add tabs to show content from other plugins that fit well with the #### Catalog Unprocessed Entities Tab +##### New Frontend System + +Create an extension and/or load a 3rd party extension to add additional tabs. + +```shell +yarn --cwd plugins/<your-plugin> add @backstage/plugin-devtools-react +``` + +```tsx +import { DevToolsContentBlueprint } from '@backstage/plugin-devtools-react'; + +export const unprocessedEntitiesDevToolsContent = DevToolsContentBlueprint.make( + { + disabled: true, + params: { + path: 'unprocessed-entities', + title: 'Unprocessed Entities', + loader: () => + import('../components/UnprocessedEntities').then(m => ( + <m.UnprocessedEntitiesContent /> + )), + }, + }, +); + +const appFeature = createFrontendModule({ + pluginId: 'catalog-unprocessed-entities', + extensions: [unprocessedEntitiesDevToolsContent], +}); +``` + +##### Old System + Here's how to add the Catalog Unprocessed Entities tab: 1. Install and setup the [Catalog Unprocessed Entities plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-unprocessed-entities) as per its documentation
plugins/devtools/report-alpha.api.md+22 −3 modified@@ -6,8 +6,10 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -59,8 +61,6 @@ const _default: OverridableFrontendPlugin< }; }>; 'page:devtools': OverridableExtensionDefinition<{ - kind: 'page'; - name: undefined; config: { path: string | undefined; }; @@ -77,7 +77,26 @@ const _default: OverridableFrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + contents: ExtensionInput< + | ConfigurableExtensionDataRef<string, 'core.title', {}> + | ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}> + | ConfigurableExtensionDataRef<string, 'core.routing.path', {}> + | ConfigurableExtensionDataRef< + RouteRef_2<AnyRouteRefParams>, + 'core.routing.ref', + { + optional: true; + } + >, + { + singleton: false; + optional: true; + } + >; + }; + kind: 'page'; + name: undefined; params: { defaultPath?: [Error: `Use the 'path' param instead`]; path: string;
plugins/devtools/report.api.md+18 −1 modified@@ -6,6 +6,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ElementType } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; @@ -28,7 +29,23 @@ export type DevToolsLayoutProps = { }; // @public (undocumented) -export const DevToolsPage: () => JSX_2.Element; +export const DevToolsPage: ({ contents }: DevToolsPageProps) => JSX_2.Element; + +// @public (undocumented) +export interface DevToolsPageContent { + // (undocumented) + children: ReactElement; + // (undocumented) + path: string; + // (undocumented) + title: string; +} + +// @public (undocumented) +export interface DevToolsPageProps { + // (undocumented) + contents?: DevToolsPageContent[]; +} // @public (undocumented) export const devToolsPlugin: BackstagePlugin<
plugins/devtools/src/alpha/plugin.tsx+31 −6 modified@@ -21,6 +21,8 @@ import { ApiBlueprint, PageBlueprint, NavItemBlueprint, + createExtensionInput, + coreExtensionData, } from '@backstage/frontend-plugin-api'; import { devToolsApiRef, DevToolsClient } from '../api'; @@ -42,12 +44,35 @@ export const devToolsApi = ApiBlueprint.make({ }); /** @alpha */ -export const devToolsPage = PageBlueprint.make({ - params: { - path: '/devtools', - routeRef: rootRouteRef, - loader: () => - import('../components/DevToolsPage').then(m => <m.DevToolsPage />), +export const devToolsPage = PageBlueprint.makeWithOverrides({ + inputs: { + contents: createExtensionInput( + [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + coreExtensionData.title, + ], + { + optional: true, + }, + ), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + path: '/devtools', + routeRef: rootRouteRef, + loader: () => { + const contents = inputs.contents.map(content => ({ + path: content.get(coreExtensionData.routePath), + title: content.get(coreExtensionData.title), + children: content.get(coreExtensionData.reactElement), + })); + return import('../components/DevToolsPage').then(m => ( + <m.DevToolsPage contents={contents} /> + )); + }, + }); }, });
plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx+14 −3 modified@@ -18,15 +18,17 @@ import { devToolsConfigReadPermission, devToolsInfoReadPermission, } from '@backstage/plugin-devtools-common'; + +import { ConfigContent } from '../Content'; import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha'; -import { ConfigContent } from '../Content/ConfigContent'; import { DevToolsLayout } from '../DevToolsLayout'; -import { InfoContent } from '../Content/InfoContent'; +import { InfoContent } from '../Content'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { ScheduledTasksContent } from '../Content/ScheduledTasksContent'; +import { DevToolsPageProps } from '../DevToolsPage'; /** @public */ -export const DefaultDevToolsPage = () => ( +export const DefaultDevToolsPage = ({ contents }: DevToolsPageProps) => ( <DevToolsLayout> <DevToolsLayout.Route path="info" title="Info"> <RequirePermission permission={devToolsInfoReadPermission}> @@ -43,5 +45,14 @@ export const DefaultDevToolsPage = () => ( <ScheduledTasksContent /> </RequirePermission> </DevToolsLayout.Route> + {contents?.map((content, index) => ( + <DevToolsLayout.Route + key={`extension-${index}`} + path={content.path} + title={content.title} + > + {content.children} + </DevToolsLayout.Route> + ))} </DevToolsLayout> );
plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx+19 −2 modified@@ -16,9 +16,26 @@ import { useOutlet } from 'react-router-dom'; import { DefaultDevToolsPage } from '../DefaultDevToolsPage'; +import { ReactElement } from 'react'; -export const DevToolsPage = () => { +/** + @public + */ +export interface DevToolsPageProps { + contents?: DevToolsPageContent[]; +} + +/** + @public + */ +export interface DevToolsPageContent { + title: string; + path: string; + children: ReactElement; +} + +export const DevToolsPage = ({ contents }: DevToolsPageProps) => { const outlet = useOutlet(); - return <>{outlet || <DefaultDevToolsPage />}</>; + return <>{outlet || <DefaultDevToolsPage contents={contents} />}</>; };
plugins/devtools/src/components/DevToolsPage/index.ts+5 −1 modified@@ -14,4 +14,8 @@ * limitations under the License. */ -export { DevToolsPage } from './DevToolsPage'; +export { + DevToolsPage, + type DevToolsPageProps, + type DevToolsPageContent, +} from './DevToolsPage';
plugins/devtools/src/components/index.ts+4 −0 modified@@ -16,3 +16,7 @@ export * from './Content'; export * from './DevToolsLayout'; +export { + type DevToolsPageProps, + type DevToolsPageContent, +} from './DevToolsPage';
yarn.lock+133 −14 modified@@ -5444,12 +5444,14 @@ __metadata: resolution: "@backstage/plugin-catalog-unprocessed-entities@workspace:plugins/catalog-unprocessed-entities" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-catalog-unprocessed-entities-common": "workspace:^" + "@backstage/plugin-devtools-react": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.60" @@ -5611,6 +5613,32 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-devtools-react@workspace:^, @backstage/plugin-devtools-react@workspace:plugins/devtools-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-devtools-react@workspace:plugins/devtools-react" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@material-ui/core": "npm:^4.9.13" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^14.0.0" + "@types/react": "npm:^18.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/plugin-devtools@workspace:^, @backstage/plugin-devtools@workspace:plugins/devtools": version: 0.0.0-use.local resolution: "@backstage/plugin-devtools@workspace:plugins/devtools" @@ -5623,6 +5651,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-devtools-common": "workspace:^" + "@backstage/plugin-devtools-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" @@ -20169,6 +20198,22 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:^9.0.0": + version: 9.3.4 + resolution: "@testing-library/dom@npm:9.3.4" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.1.3" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: 10/510da752ea76f4a10a0a4e3a77917b0302cf03effe576cd3534cab7e796533ee2b0e9fb6fb11b911a1ebd7c70a0bb6f235bf4f816c9b82b95b8fe0cddfd10975 + languageName: node + linkType: hard + "@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.6.3": version: 6.9.1 resolution: "@testing-library/jest-dom@npm:6.9.1" @@ -20205,6 +20250,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.0.0": + version: 14.3.1 + resolution: "@testing-library/react@npm:14.3.1" + dependencies: + "@babel/runtime": "npm:^7.12.5" + "@testing-library/dom": "npm:^9.0.0" + "@types/react-dom": "npm:^18.0.0" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/83359dcdf9eaf067839f34604e1a181cbc14fc09f3a07672403700fcc6a900c4b8054ad1114fc24b4b9f89d84e2a09e1b7c9afce2306b1d4b4c9e30eb1cb12de + languageName: node + linkType: hard + "@testing-library/react@npm:^16.0.0": version: 16.3.1 resolution: "@testing-library/react@npm:16.3.1" @@ -24713,6 +24772,15 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:5.1.3": + version: 5.1.3 + resolution: "aria-query@npm:5.1.3" + dependencies: + deep-equal: "npm:^2.0.5" + checksum: 10/e5da608a7c4954bfece2d879342b6c218b6b207e2d9e5af270b5e38ef8418f02d122afdc948b68e32649b849a38377785252059090d66fa8081da95d1609c0d2 + languageName: node + linkType: hard + "aria-query@npm:5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -24729,7 +24797,7 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": +"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" dependencies: @@ -25013,6 +25081,20 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10/3d49e7acbeee9e84537f4cb0e0f91893df8eba976759875ae8ee9e3d3c82f6ecdebdb347c2fad9926b92596d93cdfc78ecc988bcdf407e40433e8e8e6fe5d78e + languageName: node + linkType: hard + "async-lock@npm:^1.4.1": version: 1.4.1 resolution: "async-lock@npm:1.4.1" @@ -28491,6 +28573,32 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:^2.0.5": + version: 2.2.3 + resolution: "deep-equal@npm:2.2.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.5" + es-get-iterator: "npm:^1.1.3" + get-intrinsic: "npm:^1.2.2" + is-arguments: "npm:^1.1.1" + is-array-buffer: "npm:^3.0.2" + is-date-object: "npm:^1.0.5" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + isarray: "npm:^2.0.5" + object-is: "npm:^1.1.5" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + side-channel: "npm:^1.0.4" + which-boxed-primitive: "npm:^1.0.2" + which-collection: "npm:^1.0.1" + which-typed-array: "npm:^1.1.13" + checksum: 10/1ce49d0b71d0f14d8ef991a742665eccd488dfc9b3cada069d4d7a86291e591c92d2589c832811dea182b4015736b210acaaebce6184be356c1060d176f5a05f + languageName: node + linkType: hard + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -29686,7 +29794,7 @@ __metadata: languageName: node linkType: hard -"es-get-iterator@npm:^1.0.2": +"es-get-iterator@npm:^1.0.2, es-get-iterator@npm:^1.1.3": version: 1.1.3 resolution: "es-get-iterator@npm:1.1.3" dependencies: @@ -30700,6 +30808,7 @@ __metadata: "@backstage/plugin-catalog-import": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-catalog-unprocessed-entities": "workspace:^" + "@backstage/plugin-devtools": "workspace:^" "@backstage/plugin-home": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-kubernetes-cluster": "workspace:^" @@ -32256,6 +32365,13 @@ __metadata: languageName: node linkType: hard +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 + languageName: node + linkType: hard + "generic-names@npm:^2.0.1": version: 2.0.1 resolution: "generic-names@npm:2.0.1" @@ -32303,21 +32419,24 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": - version: 1.3.0 - resolution: "get-intrinsic@npm:1.3.0" +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 + checksum: 10/bb579dda84caa4a3a41611bdd483dade7f00f246f2a7992eb143c5861155290df3fdb48a8406efa3dfb0b434e2c8fafa4eebd469e409d0439247f85fc3fa2cc1 languageName: node linkType: hard @@ -34407,7 +34526,7 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": +"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" dependencies: @@ -34880,7 +34999,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.2.1": +"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -34929,7 +35048,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.4": +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.4": version: 1.0.4 resolution: "is-shared-array-buffer@npm:1.0.4" dependencies: @@ -44711,7 +44830,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": +"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -46279,7 +46398,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.1.0": +"side-channel@npm:^1.0.4, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -50503,7 +50622,7 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": +"which-boxed-primitive@npm:^1.0.2, which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" dependencies: @@ -50537,7 +50656,7 @@ __metadata: languageName: node linkType: hard -"which-collection@npm:^1.0.2": +"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": version: 1.0.2 resolution: "which-collection@npm:1.0.2" dependencies: @@ -50559,7 +50678,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-w669-jj7h-88m9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25152ghsaADVISORY
- github.com/backstage/backstage/commit/08f388e3394b133171fe13b62a78caf14407b9c3ghsaWEB
- github.com/backstage/backstage/security/advisories/GHSA-w669-jj7h-88m9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.