├── CODEOWNERS ├── packages ├── app │ ├── .eslintignore │ ├── src │ │ ├── components │ │ │ ├── Root │ │ │ │ ├── index.ts │ │ │ │ ├── LogoIcon.tsx │ │ │ │ ├── LogoFull.tsx │ │ │ │ └── logo │ │ │ │ │ └── kv-icon.svg │ │ │ ├── home │ │ │ │ └── logos │ │ │ │ │ ├── Argo.png │ │ │ │ │ ├── DASK.png │ │ │ │ │ ├── SKIP.png │ │ │ │ │ ├── Github.png │ │ │ │ │ ├── Google.png │ │ │ │ │ ├── Grafana.png │ │ │ │ │ ├── Sysdig.png │ │ │ │ │ ├── Databricks.png │ │ │ │ │ └── GoogleCloud.png │ │ │ └── catalog │ │ │ │ └── EntityCatalogCreatorWrapper.tsx │ │ ├── index.tsx │ │ └── apis.ts │ ├── public │ │ ├── robots.txt │ │ ├── skip.png │ │ ├── favicon.ico │ │ ├── img │ │ │ ├── jit.png │ │ │ ├── s3.jpeg │ │ │ ├── gsm.webp │ │ │ ├── argocd.png │ │ │ ├── argokit.png │ │ │ ├── github.png │ │ │ ├── grafana.png │ │ │ ├── pharos.png │ │ │ ├── sysdig.webp │ │ │ ├── nachoskip.png │ │ │ └── google-cloud.svg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── manifest.json │ │ ├── safari-pinned-tab.svg │ │ └── index.html │ ├── .eslintrc.js │ └── config.d.ts ├── backend │ ├── .eslintrc.js │ ├── src │ │ ├── plugins │ │ │ ├── extensions │ │ │ │ └── catalog.ts │ │ │ └── transformers │ │ │ │ └── msGraphTransformer.ts │ │ └── index.ts │ ├── README.md │ └── Dockerfile └── README.md ├── backstage.json ├── .eslintrc.js ├── plugins ├── function-kind-common │ ├── src │ │ ├── setupTests.ts │ │ ├── schema │ │ │ ├── index.ts │ │ │ └── kinds │ │ │ │ └── FunctionEntityV1Alpha1.schema.json │ │ └── index.ts │ ├── .eslintrc.js │ ├── README.md │ └── package.json ├── security-metrics │ ├── src │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── utils │ │ │ ├── MetricTypes.ts │ │ │ ├── isRecent.ts │ │ │ ├── parseApiDataUtils.ts │ │ │ ├── getChildRefs.ts │ │ │ └── authenticationUtils.ts │ │ ├── components │ │ │ ├── TableRow.tsx │ │ │ ├── AppContext.tsx │ │ │ ├── InfoTooltip.tsx │ │ │ ├── ErrorBanner.tsx │ │ │ ├── LightTooltip.tsx │ │ │ ├── SecretsOverview │ │ │ │ ├── SecretInfoRow.tsx │ │ │ │ ├── Secret.tsx │ │ │ │ ├── SecretsDialog.tsx │ │ │ │ └── SecretsAlert.tsx │ │ │ ├── Trend │ │ │ │ ├── LinearGradient.tsx │ │ │ │ ├── utils.ts │ │ │ │ ├── Trend.tsx │ │ │ │ ├── GraphLabels.tsx │ │ │ │ └── TrendGraph.tsx │ │ │ ├── CardTitle.tsx │ │ │ ├── FeatureToggle.tsx │ │ │ ├── VulnerabilityTable │ │ │ │ ├── ScannerTag.tsx │ │ │ │ ├── SeverityTag.tsx │ │ │ │ ├── AcceptRisk │ │ │ │ │ └── SpinnerButton.tsx │ │ │ │ ├── ScannerDetails │ │ │ │ │ ├── IdsWithUrls.tsx │ │ │ │ │ ├── DependabotContent.tsx │ │ │ │ │ ├── PharosContent.tsx │ │ │ │ │ ├── ScannerDetails.tsx │ │ │ │ │ ├── CodeQLContent.tsx │ │ │ │ │ ├── SysdigContent.tsx │ │ │ │ │ └── ScannerCard.tsx │ │ │ │ ├── utils.ts │ │ │ │ └── TableRowCollapse.tsx │ │ │ ├── RepositoriesTable │ │ │ │ ├── NoAccessRow.tsx │ │ │ │ ├── utils.ts │ │ │ │ └── RepositoryScannerStatus.tsx │ │ │ ├── ComponentVulnerabilityMttr │ │ │ │ └── ComponentVulnerabilityMttr.tsx │ │ │ ├── StarFilterButton.tsx │ │ │ ├── ScannerStatus │ │ │ │ ├── ScannerInfo.tsx │ │ │ │ ├── SystemScannerStatuses.tsx │ │ │ │ └── ComponentScannerStatus.tsx │ │ │ ├── NoAccessAlert.tsx │ │ │ ├── RosStatus │ │ │ │ └── utils.ts │ │ │ └── VulnerabilityDistribution.tsx │ │ ├── plugin.ts │ │ ├── mapping │ │ │ ├── getSecretsData.ts │ │ │ ├── getGroupedData.ts │ │ │ ├── getScannerData.ts │ │ │ └── getSeverityCounts.ts │ │ ├── hooks │ │ │ ├── useEntitiesQuery.ts │ │ │ ├── useUserProfile.ts │ │ │ ├── useComponentMetricsQuery.ts │ │ │ ├── useMetricsQuery.ts │ │ │ ├── getConfig.ts │ │ │ ├── useTrendsQuery.ts │ │ │ ├── useConfigureSlackNotificationsQuery.ts │ │ │ ├── useStarredRefFilter.ts │ │ │ ├── useAcceptVulnerabilityQuery.ts │ │ │ ├── usePagination.ts │ │ │ └── useFetchRepositoryNames.ts │ │ ├── MetricsPlugin.tsx │ │ ├── colors.ts │ │ ├── PluginRoot.tsx │ │ └── api │ │ │ └── client.ts │ ├── .eslintrc.js │ ├── dev │ │ └── index.tsx │ ├── README.md │ └── package.json ├── catalog-creator │ ├── .eslintrc.js │ ├── src │ │ ├── components │ │ │ ├── CatalogForm │ │ │ │ ├── index.ts │ │ │ │ ├── FieldHeader.tsx │ │ │ │ ├── Forms │ │ │ │ │ ├── DomainForm.tsx │ │ │ │ │ └── SystemForm.tsx │ │ │ │ └── Autocompletes │ │ │ │ │ └── TagField.tsx │ │ │ └── CatalogCreatorPage │ │ │ │ ├── index.ts │ │ │ │ ├── LoadingOverlay.tsx │ │ │ │ ├── RepositoryForm.tsx │ │ │ │ ├── SuccessMessage.tsx │ │ │ │ ├── EditOrGenerateCatalogInfoBox.tsx │ │ │ │ └── StatusMessages.tsx │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── utils │ │ │ ├── toEntityRef.ts │ │ │ ├── pageUtils.ts │ │ │ ├── getCatalogInfo.ts │ │ │ └── getRepoInfo.ts │ │ ├── plugin.ts │ │ ├── hooks │ │ │ ├── useUpdateDependentFormFields.ts │ │ │ └── useFetchEntities.ts │ │ └── catalog.module.css │ ├── dev │ │ └── index.tsx │ ├── README.md │ └── package.json ├── security-champion │ ├── .eslintrc.js │ ├── src │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── types.ts │ │ ├── components │ │ │ ├── LightTooltip.tsx │ │ │ └── ErrorBanner.tsx │ │ ├── plugin.ts │ │ ├── hooks │ │ │ ├── useUserProfile.ts │ │ │ ├── useSecurityChampionsQuery.ts │ │ │ ├── useChangeSecurityChampionsQuery.ts │ │ │ └── useChangeMultipleSecurityChampionsQuery.ts │ │ └── utils │ │ │ └── authenticationUtils.ts │ ├── dev │ │ └── index.tsx │ ├── README.md │ └── package.json ├── catalog-backend-module-function-kind │ ├── .eslintrc.js │ ├── README.md │ ├── src │ │ ├── module.ts │ │ └── index.ts │ └── package.json ├── security-metrics-backend │ ├── src │ │ ├── index.ts │ │ ├── contracts │ │ │ └── IEntraIdService.ts │ │ ├── Either.ts │ │ ├── plugin.ts │ │ ├── services │ │ │ ├── config.ts │ │ │ └── EntraIdService │ │ │ │ └── auth.service.ts │ │ └── Errors.ts │ ├── dev │ │ └── index.ts │ ├── .eslintrc.js │ ├── README.md │ └── package.json └── README.md ├── mise.toml ├── screenshot.jpeg ├── github-secrets └── github-app-backstage-skip-credentials.yaml ├── docs ├── assets │ ├── kubernetes.png │ ├── lighthouse.png │ ├── add-techdocs.png │ ├── lighthouse-2.png │ ├── grafana-alerts.png │ ├── onboarding-template.png │ └── modelleringseksempel1.png └── index.md ├── .security └── description.yaml ├── .dockerignore ├── .github ├── pull_request_template.md ├── workflows │ ├── pull_request_actions.yml │ ├── techdocs.yml │ └── deploy_to_gcp.yml └── dependabot.yml ├── lerna.json ├── .prettierignore ├── .yarnrc.yml ├── catalog-info.yaml ├── README.md ├── mkdocs.yml ├── tsconfig.json ├── app-config.runtime.yaml ├── .gitignore ├── LICENSE ├── docker-compose.yaml └── package.json /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kartverket/skvis 2 | -------------------------------------------------------------------------------- /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /backstage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.44.2" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/function-kind-common/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | # https://mise.jdx.dev/ 2 | [tools] 3 | node = "22" 4 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | export { Root } from './Root'; 2 | -------------------------------------------------------------------------------- /screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/screenshot.jpeg -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /github-secrets/github-app-backstage-skip-credentials.yaml: -------------------------------------------------------------------------------- 1 | dont: 2 | delete: 3 | this: file 4 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /docs/assets/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/kubernetes.png -------------------------------------------------------------------------------- /docs/assets/lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/lighthouse.png -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/index.ts: -------------------------------------------------------------------------------- 1 | export { securityMetricsPlugin, SecurityMetricsPage } from './plugin'; 2 | -------------------------------------------------------------------------------- /docs/assets/add-techdocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/add-techdocs.png -------------------------------------------------------------------------------- /docs/assets/lighthouse-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/lighthouse-2.png -------------------------------------------------------------------------------- /packages/app/public/skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/skip.png -------------------------------------------------------------------------------- /plugins/catalog-creator/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /docs/assets/grafana-alerts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/grafana-alerts.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/img/jit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/jit.png -------------------------------------------------------------------------------- /packages/app/public/img/s3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/s3.jpeg -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogForm/index.ts: -------------------------------------------------------------------------------- 1 | export { CatalogForm as CatalogForm } from './CatalogForm'; 2 | -------------------------------------------------------------------------------- /plugins/function-kind-common/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /plugins/security-champion/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /packages/app/public/img/gsm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/gsm.webp -------------------------------------------------------------------------------- /docs/assets/onboarding-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/onboarding-template.png -------------------------------------------------------------------------------- /packages/app/public/img/argocd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/argocd.png -------------------------------------------------------------------------------- /packages/app/public/img/argokit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/argokit.png -------------------------------------------------------------------------------- /packages/app/public/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/github.png -------------------------------------------------------------------------------- /packages/app/public/img/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/grafana.png -------------------------------------------------------------------------------- /packages/app/public/img/pharos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/pharos.png -------------------------------------------------------------------------------- /packages/app/public/img/sysdig.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/sysdig.webp -------------------------------------------------------------------------------- /docs/assets/modelleringseksempel1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/docs/assets/modelleringseksempel1.png -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/img/nachoskip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/img/nachoskip.png -------------------------------------------------------------------------------- /plugins/catalog-backend-module-function-kind/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /.security/description.yaml: -------------------------------------------------------------------------------- 1 | organization: IT 2 | product: Kartverket.dev 3 | repo_types: [InternalClient,InternalApi] 4 | platforms: [SKIP] 5 | -------------------------------------------------------------------------------- /packages/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/index.ts: -------------------------------------------------------------------------------- 1 | export { CatalogCreatorPage as CatalogCreatorPage } from './CatalogCreatorPage'; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .yarn/cache 3 | .yarn/install-state.gz 4 | node_modules 5 | packages/*/src 6 | packages/*/node_modules 7 | plugins 8 | *.local.yaml 9 | -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Argo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Argo.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/DASK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/DASK.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/SKIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/SKIP.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Github.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Google.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Grafana.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Sysdig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Sysdig.png -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/Databricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/Databricks.png -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services/ApiService/router'; 2 | export { securityMetricBackendPlugin as default } from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/app/src/components/home/logos/GoogleCloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartverket/kartverket.dev/HEAD/packages/app/src/components/home/logos/GoogleCloud.png -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/contracts/IEntraIdService.ts: -------------------------------------------------------------------------------- 1 | export interface IEntraIdService { 2 | getOboToken(token: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 🔒 Bakgrunn 2 | 3 | 4 | ## 🔑 Løsning 5 | 6 | 7 | ## 📸 Bilder 8 | 9 | | Før | Etter | 10 | | ----- | ----- | 11 | | Bilde | Bilde | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "plugins/*"], 3 | "npmClient": "yarn", 4 | "version": "0.1.0", 5 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 6 | } 7 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/index.ts: -------------------------------------------------------------------------------- 1 | export { catalogCreatorPlugin, CatalogCreatorPage } from './plugin'; 2 | export { catalogCreatorNorwegianTranslation } from './utils/translations'; 3 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'catalog-creator', 5 | }); 6 | -------------------------------------------------------------------------------- /plugins/security-champion/src/index.ts: -------------------------------------------------------------------------------- 1 | export { securityChampionPlugin, SecurityChampionPage } from './plugin'; 2 | export { SecurityChampionCard } from './components/SecurityChampionCard'; 3 | -------------------------------------------------------------------------------- /plugins/security-champion/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'security-champion', 5 | }); 6 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'security-metrics', 5 | }); 6 | -------------------------------------------------------------------------------- /plugins/function-kind-common/README.md: -------------------------------------------------------------------------------- 1 | # @internal/plugin-function-kind-common 2 | 3 | Welcome to the common package for the test-new-kind plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-types 3 | .vscode 4 | *.md 5 | 6 | # Yarn 7 | .yarnrc.yml 8 | .yarn/ 9 | 10 | # Leave ROS folder alone. 11 | .security/ 12 | 13 | # VSCode extension that backup files. 14 | .history/ -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | import KVIcon from './logo/kv-icon.svg'; 2 | 3 | const LogoIcon = () => { 4 | return Skip Icon; 5 | }; 6 | 7 | export default LogoIcon; 8 | -------------------------------------------------------------------------------- /plugins/catalog-backend-module-function-kind/README.md: -------------------------------------------------------------------------------- 1 | # @internal/plugin-catalog-backend-module-function-kind 2 | 3 | The function-kind backend module for the catalog plugin. 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | -------------------------------------------------------------------------------- /plugins/security-champion/src/types.ts: -------------------------------------------------------------------------------- 1 | export type SecurityChamp = { 2 | repositoryName: string; 3 | securityChampionEmail: string; 4 | }; 5 | 6 | export type SecurityChampionBatchUpdate = { 7 | repositoryNames: string[]; 8 | securityChampionEmail: string; 9 | }; 10 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/utils/MetricTypes.ts: -------------------------------------------------------------------------------- 1 | export enum MetricTypes { 2 | componentMetrics = 'component-metrics', 3 | metrics = 'metrics', 4 | trends = 'trends', 5 | rosStatus = 'ros-status', 6 | acceptVulnerability = 'accept-vulnerability', 7 | configureNotifications = 'configure-notifications', 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - checksum: a7960e90b20fe64de29d899d907dd9d539f4111357d84ce89eab20c01ea2941d4d4ee2a569d220be2f92f93d63cee18b85a6273cbb74980564fb80f7ee7c64d2 5 | path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs 6 | spec: "https://versions.backstage.io/v1/releases/1.44.2/yarn-plugin" 7 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # nonk8s 2 | apiVersion: 'backstage.io/v1alpha1' 3 | kind: 'Component' 4 | metadata: 5 | name: 'kartverket.dev' 6 | tags: 7 | - 'internal' 8 | annotations: 9 | backstage.io/techdocs-ref: dir:. 10 | spec: 11 | type: 'website' 12 | lifecycle: 'production' 13 | owner: 'skvis' 14 | system: 'utviklerportal' 15 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/dev/index.ts: -------------------------------------------------------------------------------- 1 | import { createBackend } from '@backstage/backend-defaults'; 2 | 3 | const backend = createBackend(); 4 | 5 | backend.add(import('@backstage/plugin-auth-backend')); 6 | backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); 7 | backend.add(import('../src')); 8 | 9 | backend.start(); 10 | -------------------------------------------------------------------------------- /plugins/security-metrics/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | 3 | module.exports = { 4 | ...base, 5 | extends: [...(base.extends ?? []), 'plugin:react/jsx-runtime'], 6 | rules: { 7 | ...(base.rules ?? {}), 8 | 'react/react-in-jsx-scope': 'off', 9 | 'react/jsx-uses-react': 'off', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/utils/isRecent.ts: -------------------------------------------------------------------------------- 1 | export function isRecent(dateFirstSeen?: string, thresholdDays = 7): boolean { 2 | if (!dateFirstSeen) return false; 3 | const t = new Date(dateFirstSeen).getTime(); 4 | if (Number.isNaN(t)) return false; 5 | const diffDays = Math.floor((Date.now() - t) / (1000 * 60 * 60 * 24)); 6 | return diffDays <= thresholdDays; 7 | } 8 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | 3 | module.exports = { 4 | ...base, 5 | extends: [...(base.extends ?? []), 'plugin:react/jsx-runtime'], 6 | rules: { 7 | ...(base.rules ?? {}), 8 | 'react/react-in-jsx-scope': 'off', 9 | 'react/jsx-uses-react': 'off', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Kartverket.dev", 3 | "name": "Kartverket.dev", 4 | "icons": [ 5 | { 6 | "src": "skip.png", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@backstage/cli/asset-types'; 2 | import App from './App'; 3 | import '@backstage/ui/css/styles.css'; 4 | import { createRoot } from 'react-dom/client'; 5 | import '@kartverket/backstage-plugin-risk-scorecard/css/theme.css'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container!); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: Index 4 | description: Introduction to Kartverket.dev 5 | --- 6 | 7 | # Kartverket.dev 8 | 9 | Welcome to the docs for Kartverket.dev! 10 | 11 | You can read more about backstage [here](https://backstage.io/docs/overview/what-is-backstage). 12 | 13 | Check out our onboarding guide [here](/getting-started/onboarding) to get your service added. 14 | -------------------------------------------------------------------------------- /plugins/security-metrics/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import { createDevApp } from '@backstage/dev-utils'; 2 | import { SecurityMetricsPage, securityMetricsPlugin } from '../src/plugin'; 3 | 4 | createDevApp() 5 | .registerPlugin(securityMetricsPlugin) 6 | .addPage({ 7 | element: , 8 | title: 'Root Page', 9 | path: '/security-metrics', 10 | }) 11 | .render(); 12 | -------------------------------------------------------------------------------- /plugins/security-champion/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import { createDevApp } from '@backstage/dev-utils'; 2 | import { securityChampionPlugin, SecurityChampionPage } from '../src/plugin'; 3 | 4 | createDevApp() 5 | .registerPlugin(securityChampionPlugin) 6 | .addPage({ 7 | element: , 8 | title: 'Root Page', 9 | path: '/security-champion', 10 | }) 11 | .render(); 12 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import TableRow, { tableRowClasses } from '@mui/material/TableRow'; 2 | import { styled } from '@mui/system'; 3 | 4 | export const StyledTableRow = styled(TableRow)(({ theme }) => ({ 5 | [`&.${tableRowClasses.root}`]: { 6 | borderBottom: `1px solid ${theme.palette.divider}`, 7 | backgroundColor: theme.palette.background.paper, 8 | }, 9 | })); 10 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # The Plugins Folder 2 | 3 | This is where your own plugins and their associated modules live, each in a 4 | separate folder of its own. 5 | 6 | If you want to create a new plugin here, go to your project root directory, run 7 | the command `yarn new`, and follow the on-screen instructions. 8 | 9 | You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! 10 | -------------------------------------------------------------------------------- /packages/app/config.d.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | auth: { 3 | providers: { 4 | /** 5 | * @visibility frontend 6 | */ 7 | microsoft: { 8 | /** 9 | * @visibility frontend 10 | */ 11 | production: { 12 | /** 13 | * @visibility frontend 14 | */ 15 | clientId: string; 16 | }; 17 | }; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface AppContextProps { 4 | isUsingNewFeature: boolean; 5 | setIsUsingNewFeature: (value: boolean) => void; 6 | } 7 | 8 | const defaultContextValue: AppContextProps = { 9 | isUsingNewFeature: false, 10 | setIsUsingNewFeature: () => {}, 11 | }; 12 | 13 | export const AppContext = createContext(defaultContextValue); 14 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # The Packages Folder 2 | 3 | This is where your own applications and centrally managed libraries live, each 4 | in a separate folder of its own. 5 | 6 | From the start there's an `app` folder (for the frontend) and a `backend` folder 7 | (for the Node backend), but you can also add more modules in here that house 8 | your core additions and adaptations, such as themes, common React component 9 | libraries, utilities, and similar. 10 | -------------------------------------------------------------------------------- /plugins/catalog-creator/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import { createDevApp } from '@backstage/dev-utils'; 2 | import { catalogCreatorPlugin, CatalogCreatorPage } from '../src/plugin'; 3 | 4 | createDevApp() 5 | .registerPlugin(catalogCreatorPlugin) 6 | .addPage({ 7 | element: ( 8 | 9 | ), 10 | title: 'Root Page', 11 | path: '/catalog-creator', 12 | }) 13 | .render(); 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Kartverket.dev](https://kartverket.dev) 2 | 3 | ![Screenshot of Kartverket.dev](./screenshot.jpeg) 4 | 5 | Kartverket.dev is Kartverket's developer portal. It is built using 6 | [Backstage](https://backstage.io/), and helps Kartverket's internal developers 7 | discover and use Kartverket's APIs and services. 8 | 9 | ## Contributing 10 | 11 | Want to contribute to the developer portal? Head over to 12 | [CONTRIBUTING.md](./CONTRIBUTING.md) to get started. 13 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Backstage 2 | site_description: 'Documentation for SKIPs Backstage' 3 | repo_url: https://github.com/kartverket/kartverket.dev 4 | edit_uri: edit/main/docs 5 | 6 | plugins: 7 | - techdocs-core 8 | 9 | nav: 10 | - Kartverket.dev: 11 | - Felles begreper: 'kartverket-dev/felles_begreper.md' 12 | - Shared concepts: 'kartverket-dev/shared_concepts.md' 13 | - Getting Started: 14 | - Adding your documentation: 'getting-started/techdocs.md' 15 | -------------------------------------------------------------------------------- /plugins/security-champion/src/components/LightTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/system'; 2 | import Tooltip, { TooltipProps } from '@mui/material/Tooltip'; 3 | 4 | export const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( 5 | 6 | ))(` 7 | font-size: 16px; 8 | max-width: 750px; 9 | padding: 8px; 10 | word-wrap: break-word; 11 | border: 1px solid gray; 12 | `); 13 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/utils/parseApiDataUtils.ts: -------------------------------------------------------------------------------- 1 | export function parseScanData(raw: unknown): Record { 2 | if (raw && typeof raw === 'object' && 'constructor' in raw) { 3 | return raw as Record; 4 | } 5 | return {}; 6 | } 7 | 8 | export function parseVulnCounts(raw: unknown): Record { 9 | if (raw && typeof raw === 'object' && 'constructor' in raw) { 10 | return raw as Record; 11 | } 12 | return {}; 13 | } 14 | -------------------------------------------------------------------------------- /plugins/function-kind-common/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON Schemas for custom entity kinds 3 | */ 4 | 5 | import functionEntityV1alpha1Schema from './kinds/FunctionEntityV1Alpha1.schema.json'; 6 | 7 | /** 8 | * Export the schema for use in processors 9 | */ 10 | export const functionEntityV1alpha1Validator = functionEntityV1alpha1Schema; 11 | 12 | /** 13 | * All schemas for this plugin 14 | */ 15 | export const schemas = { 16 | functionEntityV1alpha1: functionEntityV1alpha1Schema, 17 | } as const; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": [ 4 | "packages/*/src", 5 | "packages/*/config.d.ts", 6 | "plugins/*/src", 7 | "plugins/*/config.d.ts", 8 | "plugins/*/dev", 9 | "plugins/*/migrations" 10 | ], 11 | "exclude": ["node_modules"], 12 | "compilerOptions": { 13 | "jsx": "react-jsx", 14 | "outDir": "dist-types", 15 | "rootDir": ".", 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugins/catalog-backend-module-function-kind/src/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | coreServices, 3 | createBackendModule, 4 | } from '@backstage/backend-plugin-api'; 5 | 6 | export const catalogModuleFunctionKind = createBackendModule({ 7 | pluginId: 'catalog', 8 | moduleId: 'function-kind', 9 | register(reg) { 10 | reg.registerInit({ 11 | deps: { logger: coreServices.logger }, 12 | async init({ logger }) { 13 | logger.info('Function-processor started'); 14 | }, 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgress from '@mui/material/CircularProgress'; 2 | 3 | import style from '../../catalog.module.css'; 4 | 5 | interface LoadingOverlayProps { 6 | isDarkTheme: boolean; 7 | } 8 | 9 | export const LoadingOverlay = ({ isDarkTheme }: LoadingOverlayProps) => ( 10 |
13 | 14 |
15 | ); 16 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/InfoTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlined } from '@mui/icons-material'; 2 | import Tooltip, { TooltipProps } from '@mui/material/Tooltip'; 3 | 4 | type Props = { 5 | size?: 'inherit' | 'large' | 'medium' | 'small'; 6 | } & Omit; 7 | 8 | export const InfoTooltip = ({ size = 'small', ...rest }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/utils/getChildRefs.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@backstage/catalog-model'; 2 | 3 | export const getChildRefs = (entities: Entity[]): string[] => { 4 | return entities.flatMap( 5 | entity => 6 | entity.relations 7 | ?.filter( 8 | rel => 9 | rel.type === 'ownerOf' || 10 | rel.type === 'hasPart' || 11 | rel.type === 'parentOf' || 12 | rel.type === 'dependsOn', 13 | ) 14 | .flatMap(rel => rel.targetRef) ?? [], 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /plugins/security-champion/src/components/ErrorBanner.tsx: -------------------------------------------------------------------------------- 1 | import Alert from '@mui/material/Alert'; 2 | import AlertTitle from '@mui/material/AlertTitle'; 3 | 4 | interface ErrorBannerProps { 5 | errorTitle?: string; 6 | errorMessage?: string; 7 | } 8 | 9 | export const ErrorBanner = ({ 10 | errorTitle, 11 | errorMessage = 'En uventet feil oppsto. Vennligst prøv igjen senere.', 12 | }: ErrorBannerProps) => ( 13 | 14 | {errorTitle && {errorTitle}} 15 | {errorMessage} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/ErrorBanner.tsx: -------------------------------------------------------------------------------- 1 | import Alert from '@mui/material/Alert'; 2 | import AlertTitle from '@mui/material/AlertTitle'; 3 | 4 | interface ErrorBannerProps { 5 | errorTitle?: string; 6 | errorMessage?: string; 7 | } 8 | 9 | export const ErrorBanner = ({ 10 | errorTitle, 11 | errorMessage = 'En uventet feil oppsto. Vennligst prøv igjen senere.', 12 | }: ErrorBannerProps) => ( 13 | 14 | {errorTitle && {errorTitle}} 15 | {errorMessage} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/LightTooltip.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | export const CustomTooltip: React.ComponentType = styled( 5 | Tooltip, 6 | )(() => ({ 7 | [`& .${tooltipClasses.tooltip}`]: { 8 | color: 'black', 9 | backgroundColor: 'white', 10 | fontSize: 16, 11 | maxWidth: 750, 12 | padding: 8, 13 | wordWrap: 'break-word', 14 | border: '1px solid black', 15 | }, 16 | })); 17 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/SecretsOverview/SecretInfoRow.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import { Box } from '@mui/system'; 3 | 4 | type Props = { 5 | description: string; 6 | value: string; 7 | }; 8 | 9 | export const SecretInfoRow = ({ description, value }: Props) => { 10 | return ( 11 | 12 | 13 | {description} 14 | 15 | {value} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPlugin, 3 | createRoutableExtension, 4 | } from '@backstage/core-plugin-api'; 5 | 6 | import { rootRouteRef } from './routes'; 7 | 8 | export const securityMetricsPlugin = createPlugin({ 9 | id: 'security-metrics-frontend', 10 | routes: { 11 | root: rootRouteRef, 12 | }, 13 | }); 14 | 15 | export const SecurityMetricsPage = securityMetricsPlugin.provide( 16 | createRoutableExtension({ 17 | name: 'SecurityMetricsPage', 18 | component: () => import('./PluginRoot').then(m => m.PluginRoot), 19 | mountPoint: rootRouteRef, 20 | }), 21 | ); 22 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoFull.tsx: -------------------------------------------------------------------------------- 1 | import KartverketLogoFull from './logo/kartverket-dev.svg'; 2 | import KartverketLogoFullLight from './logo/kartverket-dev-light.svg'; 3 | 4 | type Props = { 5 | type: undefined | 'light' | 'dark'; 6 | } & React.JSX.IntrinsicAttributes & 7 | React.ClassAttributes & 8 | React.ImgHTMLAttributes; 9 | 10 | const LogoFull = (props: Props) => { 11 | const logo = 12 | props.type === 'dark' ? KartverketLogoFull : KartverketLogoFullLight; 13 | return Kartverket logo; 14 | }; 15 | 16 | export default LogoFull; 17 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/utils/toEntityRef.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from '../types/types'; 2 | 3 | export function toEntityRefList(kind: Kind, entityStrings: string[]) { 4 | return entityStrings.map(val => { 5 | if (val.toLowerCase().includes(`${kind}:default/`.toLowerCase())) { 6 | return val; 7 | } 8 | return `${kind}:default/${val}`.toLowerCase(); 9 | }); 10 | } 11 | 12 | export function toEntityRef(kind: Kind, entityString: string) { 13 | if (entityString.toLowerCase().includes(`${kind}:default/`.toLowerCase())) { 14 | return entityString; 15 | } 16 | return `${kind}:default/${entityString}`.toLowerCase(); 17 | } 18 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/Trend/LinearGradient.tsx: -------------------------------------------------------------------------------- 1 | import { SEVERITY_COLORS } from '../../colors'; 2 | 3 | interface LinearGradientProps { 4 | id: string; 5 | } 6 | 7 | export const LinearGradient = ({ id }: LinearGradientProps) => { 8 | return ( 9 | 10 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/CardTitle.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@mui/material/Paper'; 2 | import Typography from '@mui/material/Typography'; 3 | import { ReactNode } from 'react'; 4 | 5 | interface CardTitleProps { 6 | title: React.ReactNode; 7 | children?: ReactNode; 8 | } 9 | 10 | export const CardTitle = ({ title, children }: CardTitleProps) => { 11 | return ( 12 | 18 | 19 | {title} 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPlugin, 3 | createRoutableExtension, 4 | } from '@backstage/core-plugin-api'; 5 | 6 | import { rootRouteRef } from './routes'; 7 | 8 | export const catalogCreatorPlugin = createPlugin({ 9 | id: 'catalog-creator', 10 | routes: { 11 | root: rootRouteRef, 12 | }, 13 | }); 14 | 15 | export const CatalogCreatorPage = catalogCreatorPlugin.provide( 16 | createRoutableExtension({ 17 | name: 'CatalogCreatorPage', 18 | component: () => 19 | import('./components/CatalogCreatorPage/CatalogCreatorPage').then( 20 | m => m.CatalogCreatorPage, 21 | ), 22 | mountPoint: rootRouteRef, 23 | }), 24 | ); 25 | -------------------------------------------------------------------------------- /plugins/security-champion/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPlugin, 3 | createRoutableExtension, 4 | } from '@backstage/core-plugin-api'; 5 | 6 | import { rootRouteRef } from './routes'; 7 | 8 | export const securityChampionPlugin = createPlugin({ 9 | id: 'security-champion', 10 | routes: { 11 | root: rootRouteRef, 12 | }, 13 | }); 14 | 15 | export const SecurityChampionPage = securityChampionPlugin.provide( 16 | createRoutableExtension({ 17 | name: 'SecurityChampionPage', 18 | component: () => 19 | import('./components/SecurityChampionCard').then( 20 | m => m.SecurityChampionCard, 21 | ), 22 | mountPoint: rootRouteRef, 23 | }), 24 | ); 25 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/mapping/getSecretsData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Repository, 3 | RepositorySummary, 4 | SecretsOverview, 5 | } from '../typesFrontend'; 6 | 7 | export const getSecrets = ( 8 | input: RepositorySummary[] | Repository, 9 | ): SecretsOverview[] => { 10 | const repos = Array.isArray(input) ? input : [input]; 11 | return repos.map(r => ({ 12 | componentName: r.componentName, 13 | alerts: 14 | r.secrets?.alerts?.map(a => ({ 15 | createdAt: a.createdAt, 16 | summary: a.summary, 17 | secretValue: a.secretValue, 18 | htmlUrl: a.htmlUrl, 19 | bypassed: a.bypassed, 20 | bypassedBy: a.bypassedBy, 21 | })) ?? [], 22 | })); 23 | }; 24 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useEntitiesQuery.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@backstage/catalog-model'; 2 | import { useApi } from '@backstage/core-plugin-api'; 3 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | 6 | export const useEntitiesQuery = (refs: string[]) => { 7 | const catalogApi = useApi(catalogApiRef); 8 | return useQuery({ 9 | queryKey: ['entities-by-refs', ...refs], 10 | queryFn: async () => { 11 | const { items } = await catalogApi.getEntitiesByRefs({ 12 | entityRefs: refs, 13 | }); 14 | return items.filter(Boolean) as Entity[]; 15 | }, 16 | enabled: refs.length > 0, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /plugins/catalog-creator/README.md: -------------------------------------------------------------------------------- 1 | # catalog-creator 2 | 3 | Welcome to the catalog-creator plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/catalog-creator](http://localhost:3000/catalog-creator). 10 | 11 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 12 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 13 | It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. 14 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/FeatureToggle.tsx: -------------------------------------------------------------------------------- 1 | import FormControlLabel from '@mui/material/FormControlLabel'; 2 | import Switch from '@mui/material/Switch'; 3 | import { useContext } from 'react'; 4 | import { AppContext } from './AppContext'; 5 | 6 | const FeatureToggle = () => { 7 | const { isUsingNewFeature, setIsUsingNewFeature } = useContext(AppContext); 8 | return ( 9 | 13 | isUsingNewFeature 14 | ? setIsUsingNewFeature(false) 15 | : setIsUsingNewFeature(true) 16 | } 17 | /> 18 | } 19 | label="Ny feature" 20 | /> 21 | ); 22 | }; 23 | 24 | export default FeatureToggle; 25 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerTag.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import { Box } from '@mui/system'; 3 | 4 | import { Scanner } from '../../typesFrontend'; 5 | import { getScannerColor } from '../utils'; 6 | import Stack from '@mui/material/Stack'; 7 | 8 | type Props = { 9 | scanner: Scanner; 10 | }; 11 | 12 | export const ScannerTag = ({ scanner }: Props) => { 13 | return ( 14 | 15 | 21 | {scanner} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /plugins/security-champion/src/hooks/useUserProfile.ts: -------------------------------------------------------------------------------- 1 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 2 | import { useApi } from '@backstage/core-plugin-api'; 3 | import { useAsync } from 'react-use'; 4 | import { UserEntity } from '@backstage/catalog-model'; 5 | 6 | export const useUserProfile = (email: string) => { 7 | const catalogApi = useApi(catalogApiRef); 8 | 9 | const { 10 | value: user, 11 | loading, 12 | error, 13 | } = useAsync(async () => { 14 | const users = await catalogApi.getEntities({ 15 | filter: { 16 | kind: 'User', 17 | 'spec.profile.email': email, 18 | }, 19 | }); 20 | return users.items[0] as UserEntity; 21 | }, [email]); 22 | return { user, loading, error }; 23 | }; 24 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useUserProfile.ts: -------------------------------------------------------------------------------- 1 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 2 | import { useApi } from '@backstage/core-plugin-api'; 3 | import { useAsync } from 'react-use'; 4 | import { UserEntity } from '@backstage/catalog-model'; 5 | 6 | export const useUserProfile = (email: string) => { 7 | const catalogApi = useApi(catalogApiRef); 8 | 9 | const { 10 | value: user, 11 | loading, 12 | error, 13 | } = useAsync(async () => { 14 | const users = await catalogApi.getEntities({ 15 | filter: { 16 | kind: 'User', 17 | 'spec.profile.email': email, 18 | }, 19 | }); 20 | return users.items[0] as UserEntity; 21 | }, [email]); 22 | return { user, loading, error }; 23 | }; 24 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/SeverityTag.tsx: -------------------------------------------------------------------------------- 1 | import { Severity } from '../../typesFrontend'; 2 | import { getStandardSeverityFormat, SeverityColors } from '../utils'; 3 | import Chip, { ChipProps } from '@mui/material/Chip'; 4 | 5 | type Props = { 6 | severity: Severity; 7 | } & ChipProps; 8 | 9 | export const SeverityTag = ({ severity, ...rest }: Props) => { 10 | const color = SeverityColors[severity]; 11 | return ( 12 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/RepositoriesTable/NoAccessRow.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import { StyledTableRow } from '../TableRow'; 3 | import TableCell from '@mui/material/TableCell'; 4 | import Alert from '@mui/material/Alert'; 5 | 6 | type Props = { 7 | repositoryName: string; 8 | }; 9 | 10 | export const NoAccessRow = ({ repositoryName }: Props) => { 11 | return ( 12 | 13 | 14 | {repositoryName} 15 | 16 | 17 | 18 | Du har ikke tilgang til metrikker for denne komponenten 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/app/src/components/catalog/EntityCatalogCreatorWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useEntity } from '@backstage/plugin-catalog-react'; 2 | import { CatalogCreatorPage } from '@kartverket/backstage-plugin-catalog-creator'; 3 | 4 | export const EntityCatalogCreatorWrapper = () => { 5 | const { entity } = useEntity(); 6 | 7 | // Extract git URL from entity metadata 8 | const sourceLocation = 9 | entity.metadata.annotations?.['backstage.io/managed-by-origin-location']; 10 | 11 | // Remove 'url:' prefix if it exists in source-location 12 | let gitUrl = sourceLocation; 13 | if (gitUrl && gitUrl.startsWith('url:')) { 14 | gitUrl = gitUrl.substring(4); 15 | } 16 | 17 | return ( 18 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/extensions/catalog.ts: -------------------------------------------------------------------------------- 1 | import { createBackendModule } from '@backstage/backend-plugin-api'; 2 | import { microsoftGraphOrgEntityProviderTransformExtensionPoint } from '@backstage/plugin-catalog-backend-module-msgraph'; 3 | import { msGraphGroupTransformer } from '../transformers/msGraphTransformer'; 4 | 5 | export const msGroupTransformerCatalogModule = createBackendModule({ 6 | pluginId: 'catalog', 7 | moduleId: 'microsoft-graph-extensions', 8 | register(env) { 9 | env.registerInit({ 10 | deps: { 11 | microsoftGraphTransformers: 12 | microsoftGraphOrgEntityProviderTransformExtensionPoint, 13 | }, 14 | async init({ microsoftGraphTransformers }) { 15 | microsoftGraphTransformers.setGroupTransformer(msGraphGroupTransformer); 16 | }, 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_actions.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Actions 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | 10 | jobs: 11 | dependency-review: 12 | permissions: 13 | contents: read # Required for reading repository 14 | pull-requests: write # Required for dependency review comments 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Perform dependency review 21 | uses: actions/dependency-review-action@v4 22 | if: github.event_name == 'pull_request' 23 | with: 24 | comment-summary-in-pr: always 25 | fail-on-severity: moderate 26 | allow-dependencies-licenses: | 27 | pkg:github/kartverket/backstage-techdocs-action 28 | -------------------------------------------------------------------------------- /plugins/catalog-backend-module-function-kind/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createBackendModule } from '@backstage/backend-plugin-api'; 2 | import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; 3 | import { FunctionEntitiesProcessor } from './processor/FunctionEntitiesProcessor'; 4 | 5 | export const catalogModuleFunctionEntities = createBackendModule({ 6 | pluginId: 'catalog', 7 | moduleId: 'function-entities', 8 | register(env) { 9 | env.registerInit({ 10 | deps: { 11 | catalog: catalogProcessingExtensionPoint, 12 | }, 13 | async init({ catalog }) { 14 | catalog.addProcessor(new FunctionEntitiesProcessor()); 15 | }, 16 | }); 17 | }, 18 | }); 19 | 20 | export { FunctionEntitiesProcessor } from './processor/FunctionEntitiesProcessor'; 21 | export default catalogModuleFunctionEntities; 22 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/Either.ts: -------------------------------------------------------------------------------- 1 | export type Either = Left | Right; 2 | 3 | export class Left { 4 | readonly error: T; 5 | 6 | private constructor(error: T) { 7 | this.error = error; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | 18 | static create(error: U): Left { 19 | return new Left(error); 20 | } 21 | } 22 | 23 | export class Right { 24 | readonly value: T; 25 | 26 | private constructor(value: T) { 27 | this.value = value; 28 | } 29 | 30 | isLeft(): this is Left { 31 | return false; 32 | } 33 | 34 | isRight(): this is Right { 35 | return true; 36 | } 37 | 38 | static create(value: U): Right { 39 | return new Right(value); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/MetricsPlugin.tsx: -------------------------------------------------------------------------------- 1 | import { useEntity } from '@backstage/plugin-catalog-react'; 2 | import { ErrorBanner } from './components/ErrorBanner'; 3 | import { SystemPage } from './components/Views/SystemPage'; 4 | import { SingleComponentPage } from './components/Views/SingleComponentPage'; 5 | import { GroupPage } from './components/Views/GroupPage'; 6 | 7 | enum ViewType { 8 | GROUP = 'Group', 9 | SYSTEM = 'System', 10 | COMPONENT = 'Component', 11 | } 12 | 13 | export const MetricsPlugin = () => { 14 | const { entity } = useEntity(); 15 | 16 | switch (entity.kind) { 17 | case ViewType.GROUP: 18 | return ; 19 | 20 | case ViewType.SYSTEM: 21 | return ; 22 | 23 | case ViewType.COMPONENT: 24 | return ; 25 | 26 | default: 27 | return ; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/RepositoriesTable/utils.ts: -------------------------------------------------------------------------------- 1 | import { RepositorySummary } from '../../typesFrontend'; 2 | 3 | export const getScannerStatus = (repository: RepositorySummary) => [ 4 | { name: 'Dependabot', status: repository.scannerConfig.dependabot }, 5 | { name: 'CodeQL', status: repository.scannerConfig.codeQL }, 6 | { name: 'Pharos', status: repository.scannerConfig.pharos }, 7 | { name: 'Sysdig', status: repository.scannerConfig.sysdig }, 8 | ]; 9 | 10 | export const getScannersGroupedByStatus = (repository: RepositorySummary) => { 11 | const scannerStatus = getScannerStatus(repository); 12 | 13 | return { 14 | configured: scannerStatus 15 | .filter(scanner => scanner.status) 16 | .map(scanner => scanner.name), 17 | notConfigured: scannerStatus 18 | .filter(scanner => !scanner.status) 19 | .map(scanner => scanner.name), 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: 'github-actions' 10 | # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) 11 | directory: '/.github' 12 | schedule: 13 | interval: 'weekly' 14 | 15 | # Maintain dependencies for Composer 16 | - package-ecosystem: 'terraform' 17 | directory: '/terraform' 18 | schedule: 19 | interval: 'weekly' 20 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/AcceptRisk/SpinnerButton.tsx: -------------------------------------------------------------------------------- 1 | import Button, { ButtonProps } from '@mui/material/Button'; 2 | import CircularProgress from '@mui/material/CircularProgress'; 3 | 4 | type SpinnerButtonProps = ButtonProps & { 5 | loading?: boolean; 6 | spinnerSize?: number; 7 | }; 8 | 9 | export const SpinnerButton = ({ 10 | spinnerSize = 18, 11 | loading, 12 | children, 13 | onClick, 14 | }: SpinnerButtonProps) => { 15 | const handleClick = (e: React.MouseEvent) => { 16 | if (loading) return; 17 | onClick?.(e); 18 | }; 19 | 20 | return ( 21 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/colors.ts: -------------------------------------------------------------------------------- 1 | import { blue, grey, purple } from '@mui/material/colors'; 2 | 3 | export const SEVERITY_COLORS = { 4 | CRITICAL: '#910101', 5 | HIGH: '#d1081e', 6 | MEDIUM: '#f77f00', 7 | LOW: '#fdc921', 8 | NEGLIGIBLE: '#cfcfcf', 9 | UNKNOWN: '#6b6b6b', 10 | }; 11 | 12 | export const SCANNER_COLORS = { 13 | DEPENDABOT: '#0363CF', 14 | PHAROS: purple[300], 15 | SYSDIG: '#BDF78B', 16 | CODEQL: '#0f305f', 17 | TENABLE: '#4B0082', 18 | UNKNOWN: '#707070', 19 | }; 20 | 21 | export const SCANNER_CARD = { 22 | LIGHT: '#EEEEEE', 23 | DARK: '#616161', 24 | }; 25 | 26 | export const BASIC_COLORS = { 27 | BLACK: '#000000', 28 | WHITE: '#FFFFFF', 29 | DARK_GREY: grey[800], 30 | GREY: grey[700], 31 | LIGHT_GREY: grey[400], 32 | LIGHTER_GREY: grey[300], 33 | PRIMARY_LIGHT: blue[200], 34 | PRIMARY_DARK: blue[900], 35 | SUCCESS: '#2e7d32', 36 | }; 37 | 38 | export const STAR_COLOR = '#fbc02d'; 39 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/ComponentVulnerabilityMttr/ComponentVulnerabilityMttr.tsx: -------------------------------------------------------------------------------- 1 | import { CardTitle } from '../CardTitle'; 2 | import Stack from '@mui/material/Stack'; 3 | import Typography from '@mui/material/Typography'; 4 | 5 | type Props = { 6 | repositoryName: string; 7 | averageDays?: number | null; 8 | }; 9 | 10 | export const ComponentVulnerabilityMttr = ({ 11 | repositoryName, 12 | averageDays, 13 | }: Props) => { 14 | const hasData = typeof averageDays === 'number'; 15 | 16 | return ( 17 | 20 | 21 | {!hasData ? ( 22 | 23 | Ingen data 24 | 25 | ) : ( 26 | {averageDays.toFixed(1)} dager 27 | )} 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /app-config.runtime.yaml: -------------------------------------------------------------------------------- 1 | # Reference documentation http://backstage.io/docs/features/techdocs/configuration 2 | # Note: After experimenting with basic setup, use CI/CD to generate docs 3 | # and an external cloud storage when deploying TechDocs for production use-case. 4 | # https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach 5 | techdocs: 6 | builder: 'external' 7 | publisher: 8 | type: 'googleGcs' 9 | googleGcs: 10 | bucketName: ${TECHDOCS_BUCKET_NAME} 11 | credentials: 12 | $file: /var/run/secrets/tokens/gcp-ksa/google-application-credentials.json 13 | projectId: ${TECHDOCS_PROJECT_ID} 14 | 15 | backend: 16 | database: 17 | connection: 18 | ssl: 19 | ca: 20 | $file: /app/db-ssl/server.crt 21 | cert: 22 | $file: /app/db-ssl/client.crt 23 | key: 24 | $file: /app/db-ssl/client.key 25 | -------------------------------------------------------------------------------- /plugins/function-kind-common/src/schema/kinds/FunctionEntityV1Alpha1.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "FunctionEntityV1alpha1", 4 | "description": "A Function describes a function instance in your infrastructure", 5 | 6 | "allOf": [ 7 | { 8 | "$ref": "Entity" 9 | }, 10 | { 11 | "type": "object", 12 | "required": ["apiVersion", "kind", "spec"], 13 | "properties": { 14 | "apiVersion": { 15 | "enum": ["kartverket.dev/v1alpha1"] 16 | }, 17 | "kind": { 18 | "enum": ["Function"] 19 | }, 20 | "spec": { 21 | "type": "object", 22 | "required": ["owner"], 23 | "properties": { 24 | "owner": { 25 | "type": "string", 26 | "description": "An entity reference to the owner of the function", 27 | "minLength": 1 28 | } 29 | } 30 | } 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Dependencies 13 | node_modules/ 14 | 15 | # VSCode extension 16 | .history/ 17 | 18 | .vscode/settings.json 19 | 20 | # Yarn files 21 | .pnp.* 22 | .yarn/* 23 | !.yarn/patches 24 | !.yarn/plugins 25 | !.yarn/releases 26 | !.yarn/sdks 27 | !.yarn/versions 28 | 29 | # Node version directives 30 | .nvmrc 31 | 32 | # dotenv environment variables file 33 | .env 34 | 35 | # Build output 36 | dist 37 | dist-types 38 | 39 | # Temporary change files created by Vim 40 | *.swp 41 | 42 | # MkDocs build output 43 | site 44 | 45 | # VSCode 46 | .vscode/settings.json 47 | 48 | # Local configuration files 49 | *.local.yaml 50 | 51 | # vscode database functionality support files 52 | *.session.sql 53 | 54 | #intellij 55 | .idea 56 | 57 | #sqlite 58 | *.sqlite 59 | *.sqlite-journal 60 | 61 | github-app-backstage-skip-credentials.yaml 62 | .terraform/ 63 | terraform_local/ -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/IdsWithUrls.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@backstage/core-components'; 2 | import { VulnerabilityIdInfo } from '../../../typesFrontend'; 3 | 4 | export interface IdsWithURLsProps { 5 | vulnerabilityIdInfo: VulnerabilityIdInfo[]; 6 | } 7 | 8 | export const IdsWithUrls = ({ vulnerabilityIdInfo }: IdsWithURLsProps) => { 9 | const idInfoWithUrl = vulnerabilityIdInfo.filter( 10 | id => id.url !== undefined && id.url !== null, 11 | ); 12 | 13 | if (idInfoWithUrl.length === 0) { 14 | return ( 15 | <> 16 | ID(s): 17 | {vulnerabilityIdInfo.map(vulnerability => vulnerability.id).join(', ')} 18 | 19 | ); 20 | } 21 | return ( 22 | <> 23 | Link(s): 24 | {idInfoWithUrl.map(id => ( 25 | <> 26 | {idInfoWithUrl.length > 1 &&
} 27 | {id.id} 28 | 29 | ))} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogForm/FieldHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from '@backstage/ui'; 2 | import { Tooltip } from '@material-ui/core'; 3 | import { InfoOutlined } from '@mui/icons-material'; 4 | 5 | import style from '../../catalog.module.css'; 6 | 7 | type FieldHeaderProps = { 8 | fieldName: string; 9 | tooltipText?: string; 10 | required?: boolean; 11 | }; 12 | 13 | export const FieldHeader = ({ 14 | fieldName, 15 | tooltipText, 16 | required, 17 | }: FieldHeaderProps) => { 18 | return ( 19 | 20 |

21 | {fieldName} 22 | 23 | {required && *} 24 | {tooltipText && ( 25 | 26 |

27 | 28 |
29 |
30 | )} 31 |

32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | coreServices, 3 | createBackendPlugin, 4 | } from '@backstage/backend-plugin-api'; 5 | import { createRouter } from './services/ApiService/router'; 6 | 7 | /** 8 | * securityMetricBackendPlugin backend plugin 9 | * 10 | * @public 11 | */ 12 | export const securityMetricBackendPlugin = createBackendPlugin({ 13 | pluginId: 'security-metrics', 14 | register(env) { 15 | env.registerInit({ 16 | deps: { 17 | httpRouter: coreServices.httpRouter, 18 | auth: coreServices.auth, 19 | logger: coreServices.logger, 20 | config: coreServices.rootConfig, 21 | }, 22 | async init({ httpRouter, auth, logger, config }) { 23 | httpRouter.use( 24 | await createRouter({ 25 | auth, 26 | logger, 27 | config, 28 | }), 29 | ); 30 | httpRouter.addAuthPolicy({ 31 | path: '/proxy', 32 | allow: 'unauthenticated', 33 | }); 34 | }, 35 | }); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/DependabotContent.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | import { Vulnerability } from '../../../typesFrontend'; 4 | import { Link } from '@backstage/core-components'; 5 | import { ErrorBanner } from '../../ErrorBanner'; 6 | import { IdsWithUrls } from './IdsWithUrls'; 7 | 8 | interface DependabotContentProps { 9 | vulnerability: Vulnerability; 10 | } 11 | 12 | export const DependabotContent = ({ 13 | vulnerability, 14 | }: DependabotContentProps) => { 15 | const info = vulnerability.scannerSpecificInfo.dependabotInfo; 16 | if (!info) { 17 | return ; 18 | } 19 | const link = new URL(info?.htmlUrl).href; 20 | 21 | return ( 22 | 23 | 24 |
25 | GitHub-link: 26 | 27 | {link.length > 80 ? link.slice(0, 30).concat('...') : link} 28 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/techdocs.yml: -------------------------------------------------------------------------------- 1 | name: Publish TechDocs Site 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docs/**' 7 | - 'mkdocs.yml' 8 | - '.github/workflows/techdocs.yml' 9 | 10 | jobs: 11 | publish-techdocs-site: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | id-token: write 17 | # The following secrets are required in your CI environment for publishing files to AWS S3. 18 | # e.g. You can use GitHub Organization secrets to set them for all existing and new repositories. 19 | 20 | steps: 21 | - id: 'techdocs-action' 22 | uses: kartverket/backstage-techdocs-action@v1.1 23 | with: 24 | entity_kind: 'component' 25 | entity_name: 'kartverket.dev' 26 | gcs_bucket_name: ${{vars.BACKSTAGE_TECHDOCS_GCS_BUCKET_NAME}} 27 | workload_identity_provider: ${{vars.BACKSTAGE_TECHDOCS_WIF}} 28 | service_account: ${{vars.BACKSTAGE_TECHDOCS_SERVICE_ACCOUNT}} 29 | project_id: ${{vars.BACKSTAGE_TECHDOCS_PROJECT_ID}} 30 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/PharosContent.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | import { Vulnerability } from '../../../typesFrontend'; 4 | import { Link } from '@backstage/core-components'; 5 | import { ErrorBanner } from '../../ErrorBanner'; 6 | import { IdsWithUrls } from './IdsWithUrls'; 7 | 8 | interface PharosContentProps { 9 | vulnerability: Vulnerability; 10 | } 11 | 12 | export const PharosContent = ({ vulnerability }: PharosContentProps) => { 13 | const info = vulnerability.scannerSpecificInfo.pharosInfo; 14 | if (!info) { 15 | return ; 16 | } 17 | const link = new URL(info?.htmlUrl).href; 18 | 19 | return ( 20 | 21 | 22 |
23 | GitHub-link: 24 | 25 | {link.length > 80 ? link.slice(0, 30).concat('...') : link} 26 | 27 |
28 | Branch: 29 | {info.branch} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kartverket 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/function-kind-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-function-kind-common", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "description": "Common functionalities for the function-kind plugin", 7 | "main": "src/index.ts", 8 | "types": "src/index.ts", 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "dist/index.cjs.js", 12 | "module": "dist/index.esm.js", 13 | "types": "dist/index.d.ts" 14 | }, 15 | "backstage": { 16 | "role": "common-library", 17 | "pluginId": "function-kind" 18 | }, 19 | "sideEffects": false, 20 | "scripts": { 21 | "build": "backstage-cli package build", 22 | "lint": "backstage-cli package lint", 23 | "test": "backstage-cli package test", 24 | "clean": "backstage-cli package clean", 25 | "prepack": "backstage-cli package prepack", 26 | "postpack": "backstage-cli package postpack" 27 | }, 28 | "devDependencies": { 29 | "@backstage/cli": "backstage:^" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "dependencies": { 35 | "@backstage/catalog-model": "backstage:^" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/StarFilterButton.tsx: -------------------------------------------------------------------------------- 1 | import { Star, StarBorder } from '@mui/icons-material'; 2 | import IconButton from '@mui/material/IconButton'; 3 | import Tooltip from '@mui/material/Tooltip'; 4 | import { FilterEnum } from '../typesFrontend'; 5 | import { STAR_COLOR } from '../colors'; 6 | 7 | type Props = { 8 | hasStarred: boolean; 9 | effectiveFilter: FilterEnum; 10 | onToggle: () => void; 11 | }; 12 | 13 | export const StarFilterButton = ({ 14 | hasStarred, 15 | effectiveFilter, 16 | onToggle, 17 | }: Props) => { 18 | const isStarred = effectiveFilter === 'starred'; 19 | 20 | let title = 'Du har ingen stjernemarkerte komponenter'; 21 | if (hasStarred && isStarred) { 22 | title = 'Vis alle komponenter'; 23 | } else if (hasStarred && !isStarred) { 24 | title = 'Vis stjernemarkerte komponenter'; 25 | } 26 | 27 | return ( 28 | 29 | 30 | 31 | {isStarred ? : } 32 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/ScannerDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Scanner, Vulnerability } from '../../../typesFrontend'; 2 | import { ScannerCard } from './ScannerCard'; 3 | import { CodeQLContent } from './CodeQLContent'; 4 | import { DependabotContent } from './DependabotContent'; 5 | import { PharosContent } from './PharosContent'; 6 | import { SysdigContent } from './SysdigContent'; 7 | 8 | type ScannerProps = { 9 | vulnerability: Vulnerability; 10 | scanner: Scanner; 11 | }; 12 | 13 | export const ScannerDetails = ({ vulnerability, scanner }: ScannerProps) => { 14 | return ( 15 | 16 | {scanner === Scanner.CodeQL && ( 17 | 18 | )} 19 | {scanner === Scanner.Dependabot && ( 20 | 21 | )} 22 | {scanner === Scanner.Sysdig && ( 23 | 24 | )} 25 | {scanner === Scanner.Pharos && ( 26 | 27 | )} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/utils/pageUtils.ts: -------------------------------------------------------------------------------- 1 | import { AnalyzeResult } from '@backstage/plugin-catalog-import'; 2 | 3 | /** 4 | * Scrolls to the top of the page smoothly 5 | */ 6 | export const scrollToTop = () => { 7 | const article = document.querySelector('article'); 8 | if (article && article.parentElement) { 9 | article.parentElement.scrollTo({ top: 0, behavior: 'smooth' }); 10 | } 11 | }; 12 | 13 | /** 14 | * Extracts the repository name from a GitHub URL 15 | * @param url - The GitHub repository URL 16 | * @returns The repository name or empty string if not found 17 | */ 18 | export const getDefaultNameFromUrl = (url: string): string => { 19 | const regexMatch = url.match(/github\.com\/([^\/]+)\/([^\/]+)/); 20 | return regexMatch?.[2] || ''; 21 | }; 22 | 23 | /** 24 | * Gets the submit URL from analysis result 25 | * @param analysisResult - The analysis result from catalog import API 26 | * @returns The URL to submit to 27 | */ 28 | export const getSubmitUrl = (analysisResult: AnalyzeResult): string => { 29 | if (analysisResult.type === 'locations') { 30 | return analysisResult.locations[0].target; 31 | } 32 | return analysisResult.url; 33 | }; 34 | -------------------------------------------------------------------------------- /plugins/security-metrics/README.md: -------------------------------------------------------------------------------- 1 | # Security metrics (frontend-plugin) 2 | 3 | This is a frontend plugin for the Security Metrics Backstage plugin. It gives an overview of security-metrics delivered 4 | by scanners on repositories owned by kartverket. The plugin is dependent on a associated backend-plugin for Backstage 5 | and a separate kotlin/spring-boot backend. The package should be added in the `packages/app`directory 6 | 7 | > **_NOTE:_** Ensure that you have installed and configured the backend plugin as 8 | > well: [@kartverket/backstage-plugin-security-metrics-backend](https://www.npmjs.com/package/@kartverket) 9 | 10 | ### kartverket.dev configuration 11 | 12 | To show the tab introduced by the plugin, add the following component to the needed pages in `EntityPage.tsx`. 13 | 14 | ```typescript 15 | 16 | 17 | 18 | ``` 19 | 20 | ### Links 21 | 22 | - [Github repository for both plugins](https://github.com/kartverket/sikkerhetsmetrikker-plugin) 23 | 24 | - [Github repository for backend](https://github.com/kartverket/sikkerhetsmetrikker-plugin-backend) 25 | -------------------------------------------------------------------------------- /plugins/security-champion/src/hooks/useSecurityChampionsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { SecurityChamp } from '../types'; 4 | import { post } from '../api/client'; 5 | import { 6 | configApiRef, 7 | identityApiRef, 8 | useApi, 9 | } from '@backstage/core-plugin-api'; 10 | import { getBackstageToken } from '../utils/authenticationUtils'; 11 | 12 | export const useSecurityChampionsQuery = (repositoryNames: string[]) => { 13 | const backendUrl = useApi(configApiRef).getString('backend.baseUrl'); 14 | const backstageAuthApi = useApi(identityApiRef); 15 | 16 | return useQuery({ 17 | queryKey: ['security-champions', repositoryNames], 18 | queryFn: async () => { 19 | const { backstageToken } = await getBackstageToken(backstageAuthApi); 20 | 21 | const endpointUrl = `${backendUrl}/api/proxy/security-champion-proxy/api/securityChampion`; 22 | return post<{ repositoryNames: string[] }, SecurityChamp[]>( 23 | endpointUrl, 24 | backstageToken, 25 | { repositoryNames }, 26 | ); 27 | }, 28 | enabled: repositoryNames.length !== 0, 29 | staleTime: 3600000, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/CodeQLContent.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | import { Vulnerability } from '../../../typesFrontend'; 4 | import { Link } from '@backstage/core-components'; 5 | import { ErrorBanner } from '../../ErrorBanner'; 6 | import { IdsWithUrls } from './IdsWithUrls'; 7 | 8 | interface CodeQLContentProps { 9 | vulnerability: Vulnerability; 10 | } 11 | 12 | export const CodeQLContent = ({ vulnerability }: CodeQLContentProps) => { 13 | const info = vulnerability.scannerSpecificInfo.codeQLInfo; 14 | if (!info) { 15 | return ; 16 | } 17 | const link = new URL(info?.htmlUrl).href; 18 | 19 | return ( 20 | 21 | 22 |
23 | GitHub-link: 24 | 25 | {link.length > 80 ? link.slice(0, 30).concat('...') : link} 26 | 27 |
28 | Branches: 29 | {info.branch} 30 |
31 | Number of locations: 32 | {info.locations} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/utils/authenticationUtils.ts: -------------------------------------------------------------------------------- 1 | import { IdentityApi, OAuthApi } from '@backstage/core-plugin-api'; 2 | import { Config } from '@backstage/config'; 3 | 4 | export async function getAuthenticationTokens( 5 | config: Config, 6 | backstageAuthApi: IdentityApi, 7 | microsoftAuthApi: OAuthApi, 8 | ): Promise<{ entraIdToken: string; backstageToken: string }> { 9 | const environment = config.getString('auth.environment'); 10 | const clientId = config.getString( 11 | `auth.providers.microsoft.${environment}.clientId`, 12 | ); 13 | 14 | const backstageToken = (await backstageAuthApi.getCredentials()).token; 15 | if (!backstageToken) { 16 | throw new Error('Backstage token could not be retrieved.'); 17 | } 18 | 19 | let entraIdToken: string; 20 | try { 21 | const token = await microsoftAuthApi.getAccessToken(`${clientId}/.default`); 22 | if (!token) { 23 | throw new Error('Entra ID token is undefined.'); 24 | } 25 | entraIdToken = token; 26 | } catch (error) { 27 | throw new Error( 28 | `Failed to get Entra ID token: ${error instanceof Error ? error.message : String(error)}`, 29 | ); 30 | } 31 | 32 | return { entraIdToken, backstageToken }; 33 | } 34 | -------------------------------------------------------------------------------- /plugins/security-champion/README.md: -------------------------------------------------------------------------------- 1 | # Security champion plugin 2 | 3 | This security champion plugin displays the security champion of a repository in the Kartverket.dev developer portal and enables changing the security champion from within backstage. The plugin enables anyone with access to a github repository to search through the users in the user catalog by email and set a user as security champion. The plugin is dependent on the (Security Champion API)[https://github.com/kartverket/security-champion-api]. 4 | 5 | ## Run the plugin 6 | The security champion plugin is hosted as an npm package and is imported into Kartverket.dev. It is a frontend backstage plugin implying that react components can be imported directly into the pages they are used. In order for the frontend to connect to the Security Champion API, running locally, a local proxy address must be defined in app-config.yaml: 7 | 8 | ``` 9 | '/security-champion-proxy': 10 | target: http://localhost:8080 11 | ``` 12 | 13 | Kartverket.dev is set up using microsoft authentication, and authentication is necessary for the plugin to attach a valid backstage token to the proxy API. Assuming this is in order, the application can be run using `yarn install` followed by `yarn dev`. 14 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/Trend/utils.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from 'date-fns'; 2 | import { TrendSeverityCounts } from '../../typesFrontend'; 3 | 4 | /** This function aggregates the trends by date by summing the severity counts for each date. 5 | * @param trends - The trends to aggregate 6 | * @returns: TrendSeverityCounts[] - List of aggregated trends 7 | */ 8 | export const getAggregatedTrends = (trends: TrendSeverityCounts[]) => { 9 | const aggregated: Record = {}; 10 | 11 | trends.forEach(item => { 12 | const date = formatDate(item.timestamp, 'yyyy-MM-dd'); // Get just the date 13 | 14 | if (!aggregated[date]) { 15 | aggregated[date] = { 16 | timestamp: date, 17 | unknown: 0, 18 | low: 0, 19 | medium: 0, 20 | high: 0, 21 | critical: 0, 22 | negligible: 0, 23 | }; 24 | } 25 | aggregated[date].unknown += item.unknown; 26 | aggregated[date].negligible += item.negligible; 27 | aggregated[date].low += item.low; 28 | aggregated[date].medium += item.medium; 29 | aggregated[date].high += item.high; 30 | aggregated[date].critical += item.critical; 31 | }); 32 | 33 | return Object.values(aggregated); 34 | }; 35 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useComponentMetricsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getAuthenticationTokens } from '../utils/authenticationUtils'; 3 | import { MetricTypes } from '../utils/MetricTypes'; 4 | import { useConfig } from './getConfig'; 5 | import { Repository } from '../typesFrontend'; 6 | import { get } from '../api/client'; 7 | 8 | export const componentMetricsQueryKeys = { 9 | metrics: (componentName: string) => ['metrics', componentName], 10 | }; 11 | 12 | export const useComponentMetricsQuery = (componentName: string) => { 13 | const { config, backstageAuthApi, microsoftAuthApi, endpointUrl } = useConfig( 14 | MetricTypes.componentMetrics, 15 | ); 16 | 17 | return useQuery({ 18 | queryKey: componentMetricsQueryKeys.metrics(componentName), 19 | queryFn: async () => { 20 | const { entraIdToken, backstageToken } = await getAuthenticationTokens( 21 | config, 22 | backstageAuthApi, 23 | microsoftAuthApi, 24 | ); 25 | endpointUrl.searchParams.set('componentName', componentName); 26 | return get(endpointUrl, backstageToken, entraIdToken); 27 | }, 28 | retry: 1, 29 | staleTime: 3600000, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/services/config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@backstage/config'; 2 | 3 | export type BackendConfig = { 4 | environment: string; 5 | isUsingMock: boolean; 6 | backendBaseUrl: string; 7 | entraIdConfig: EntraIdConfig; 8 | }; 9 | 10 | export type EntraIdConfig = { 11 | tenantId: string; 12 | clientId: string; 13 | clientSecret: string; 14 | scope: string; 15 | }; 16 | 17 | export const getBackendConfig = (config: Config): BackendConfig => { 18 | const environment = config.getString('auth.environment'); 19 | const clientCredentialsConfig = `auth.providers.microsoft.${environment}`; 20 | const securityMetricsConfig = 'sikkerhetsmetrikker'; 21 | return { 22 | environment: environment, 23 | isUsingMock: !!config.getOptionalBoolean('isUsingMock'), 24 | backendBaseUrl: config.getString(`${securityMetricsConfig}.baseUrl`), 25 | entraIdConfig: { 26 | tenantId: config.getString(`${clientCredentialsConfig}.tenantId`), 27 | clientId: config.getString(`${clientCredentialsConfig}.clientId`), 28 | clientSecret: config.getString(`${clientCredentialsConfig}.clientSecret`), 29 | scope: `${config.getOptionalString( 30 | `${securityMetricsConfig}.clientId`, 31 | )}/.default`, 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/mapping/getGroupedData.ts: -------------------------------------------------------------------------------- 1 | import { Secrets } from '../components/SecretsOverview/SecretsDialog'; 2 | import { 3 | SikkerhetsmetrikkerSystemTotal, 4 | RepositorySummary, 5 | } from '../typesFrontend'; 6 | 7 | export const getAllSecrets = ( 8 | data: SikkerhetsmetrikkerSystemTotal[], 9 | ): Secrets[] => 10 | data 11 | .flatMap(s => s.metrics?.permittedMetrics ?? []) 12 | .map(r => ({ 13 | componentName: r.componentName, 14 | alerts: r.secrets?.alerts ?? [], 15 | })) 16 | .filter(s => s.alerts.length > 0); 17 | 18 | export const getAllPermittedMetrics = ( 19 | data: SikkerhetsmetrikkerSystemTotal[], 20 | ): RepositorySummary[] => data.flatMap(s => s.metrics?.permittedMetrics ?? []); 21 | 22 | export const getAllNotPermittedComponents = ( 23 | data: SikkerhetsmetrikkerSystemTotal[], 24 | ): string[] => data.flatMap(s => s.metrics?.notPermittedComponents ?? []); 25 | 26 | export type NotPermittedInfo = { 27 | systemName: string; 28 | components: string[]; 29 | }; 30 | 31 | export const getNotPermittedInfo = ( 32 | data: SikkerhetsmetrikkerSystemTotal[], 33 | ): NotPermittedInfo[] => 34 | data 35 | .map(s => ({ 36 | systemName: s.systemName, 37 | components: s.metrics?.notPermittedComponents ?? [], 38 | })) 39 | .filter(s => s.components.length > 0); 40 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/RepositoryForm.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, Button, Box, Flex } from '@backstage/ui'; 2 | import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; 3 | import { catalogCreatorTranslationRef } from '../../utils/translations'; 4 | 5 | interface RepositoryFormProps { 6 | url: string; 7 | onUrlChange: (url: string) => void; 8 | onSubmit: (e: React.FormEvent) => void; 9 | disableTextField: boolean; 10 | } 11 | 12 | export const RepositoryForm = ({ 13 | url, 14 | onUrlChange, 15 | onSubmit, 16 | disableTextField, 17 | }: RepositoryFormProps) => { 18 | const { t } = useTranslationRef(catalogCreatorTranslationRef); 19 | 20 | return ( 21 |
22 | 23 | 24 |
25 | 34 |
35 | 36 |
37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useMetricsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getAuthenticationTokens } from '../utils/authenticationUtils'; 3 | import { MetricTypes } from '../utils/MetricTypes'; 4 | import { useConfig } from './getConfig'; 5 | import { SikkerhetsmetrikkerSystemTotal } from '../typesFrontend'; 6 | import { post } from '../api/client'; 7 | 8 | export const metricsQueryKeys = { 9 | metrics: (componentNames: string[]) => ['metrics', componentNames], 10 | }; 11 | 12 | export const useMetricsQuery = (componentNames: string[]) => { 13 | const { config, backstageAuthApi, microsoftAuthApi, endpointUrl } = useConfig( 14 | MetricTypes.metrics, 15 | ); 16 | 17 | return useQuery({ 18 | queryKey: metricsQueryKeys.metrics(componentNames), 19 | queryFn: async () => { 20 | const { entraIdToken, backstageToken } = await getAuthenticationTokens( 21 | config, 22 | backstageAuthApi, 23 | microsoftAuthApi, 24 | ); 25 | return post< 26 | { componentNames: string[]; entraIdToken: string }, 27 | SikkerhetsmetrikkerSystemTotal[] 28 | >(endpointUrl, backstageToken, { componentNames, entraIdToken }); 29 | }, 30 | retry: 1, 31 | enabled: componentNames.length !== 0, 32 | staleTime: 3600000, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | Created by potrace 1.11, written by Peter Selinger 2001-2013 -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/getConfig.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useApi, 3 | configApiRef, 4 | identityApiRef, 5 | microsoftAuthApiRef, 6 | BackstageIdentityApi, 7 | IdentityApi, 8 | OAuthApi, 9 | OpenIdConnectApi, 10 | ProfileInfoApi, 11 | SessionApi, 12 | } from '@backstage/core-plugin-api'; 13 | import { MetricTypes } from '../utils/MetricTypes'; 14 | import { Config } from '@backstage/config'; 15 | 16 | type UseConfigReturn = { 17 | config: Config; 18 | backstageAuthApi: IdentityApi; 19 | microsoftAuthApi: OAuthApi & 20 | OpenIdConnectApi & 21 | ProfileInfoApi & 22 | BackstageIdentityApi & 23 | SessionApi; 24 | backendUrl: string; 25 | endpointUrl: URL; 26 | }; 27 | 28 | export const useConfig = (type: MetricTypes): UseConfigReturn => { 29 | const config = useApi(configApiRef); 30 | const backstageAuthApi = useApi(identityApiRef); 31 | const microsoftAuthApi = useApi(microsoftAuthApiRef); 32 | const backendUrl = config.getString('backend.baseUrl'); 33 | 34 | const endpointUrl = new URL( 35 | type === MetricTypes.acceptVulnerability || 36 | type === MetricTypes.configureNotifications 37 | ? `${backendUrl}/api/security-metrics/proxy/${type}` 38 | : `${backendUrl}/api/security-metrics/proxy/fetch-${type}`, 39 | ); 40 | return { 41 | config, 42 | backstageAuthApi, 43 | microsoftAuthApi, 44 | backendUrl, 45 | endpointUrl, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/ScannerStatus/ScannerInfo.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from '@mui/material/Tooltip'; 2 | import Typography from '@mui/material/Typography'; 3 | import InfoIcon from '@mui/icons-material/Info'; 4 | import type { Scanner } from '../../typesFrontend'; 5 | import { BASIC_COLORS } from '../../colors'; 6 | import Box from '@mui/material/Box'; 7 | 8 | const scannerTooltips: Record = { 9 | Dependabot: 10 | 'Scanner kodeavhengigheter for kjente sårbarheter. Bør være aktivert på alle repoer som ikke er av typen "documentation".', 11 | CodeQL: 'Analyserer selve kildekoden for sikkerhets- og konfigurasjonsfeil.', 12 | Pharos: 13 | 'Scanner docker images og infrastruktur for sårbarheter. Krever at repoet bygger en container.', 14 | Sysdig: 15 | 'Scanner tjenester som kjører på SKIP for sårbarheter. Ikke relevant dersom koden ikke kan/skal kjøre på SKIP.', 16 | }; 17 | 18 | type Props = { 19 | name: Scanner; 20 | }; 21 | 22 | export const ScannerInfo = ({ name }: Props) => ( 23 | 24 | {name} 25 | 26 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /plugins/catalog-backend-module-function-kind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-catalog-backend-module-function-kind", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "description": "The function-kind backend module for the catalog plugin.", 7 | "main": "src/index.ts", 8 | "types": "src/index.ts", 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "dist/index.cjs.js", 12 | "types": "dist/index.d.ts" 13 | }, 14 | "backstage": { 15 | "role": "backend-plugin-module", 16 | "pluginId": "catalog" 17 | }, 18 | "scripts": { 19 | "start": "backstage-cli package start", 20 | "build": "backstage-cli package build", 21 | "lint": "backstage-cli package lint", 22 | "test": "backstage-cli package test", 23 | "clean": "backstage-cli package clean", 24 | "prepack": "backstage-cli package prepack", 25 | "postpack": "backstage-cli package postpack" 26 | }, 27 | "dependencies": { 28 | "@backstage/backend-plugin-api": "backstage:^", 29 | "@backstage/catalog-model": "backstage:^", 30 | "@backstage/plugin-catalog-common": "backstage:^", 31 | "@backstage/plugin-catalog-node": "backstage:^", 32 | "@internal/plugin-function-kind-common": "workspace:^" 33 | }, 34 | "devDependencies": { 35 | "@backstage/backend-test-utils": "backstage:^", 36 | "@backstage/cli": "backstage:^" 37 | }, 38 | "files": [ 39 | "dist" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useTrendsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getAuthenticationTokens } from '../utils/authenticationUtils'; 3 | import { MetricTypes } from '../utils/MetricTypes'; 4 | import { useConfig } from './getConfig'; 5 | import { TrendSeverityCounts } from '../typesFrontend'; 6 | import { post } from '../api/client'; 7 | 8 | export const useTrendsQuery = ( 9 | componentNames: string[], 10 | fromDate: Date, 11 | toDate: Date, 12 | ) => { 13 | const { config, backstageAuthApi, microsoftAuthApi, endpointUrl } = useConfig( 14 | MetricTypes.trends, 15 | ); 16 | 17 | return useQuery({ 18 | queryKey: ['trends', [...componentNames], fromDate, toDate], 19 | queryFn: async () => { 20 | const { entraIdToken, backstageToken } = await getAuthenticationTokens( 21 | config, 22 | backstageAuthApi, 23 | microsoftAuthApi, 24 | ); 25 | return post< 26 | { 27 | componentNames: string[]; 28 | fromDate: Date; 29 | toDate: Date; 30 | entraIdToken: string; 31 | }, 32 | TrendSeverityCounts[] 33 | >(endpointUrl, backstageToken, { 34 | componentNames, 35 | fromDate, 36 | toDate, 37 | entraIdToken, 38 | }); 39 | }, 40 | staleTime: 3600000, 41 | refetchOnWindowFocus: false, 42 | enabled: componentNames.length > 0, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/transformers/msGraphTransformer.ts: -------------------------------------------------------------------------------- 1 | import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; 2 | import { GroupEntity, GroupEntityV1alpha1 } from '@backstage/catalog-model'; 3 | import { defaultGroupTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; 4 | 5 | export async function msGraphGroupTransformer( 6 | group: MicrosoftGraph.Group, 7 | groupPhoto?: string, 8 | ): Promise { 9 | if (group.displayName?.includes('_leads')) { 10 | return undefined; 11 | } 12 | const groupType = group.displayName?.split(' - ')[2]; 13 | 14 | if (group.displayName?.toLowerCase().includes('aad - tf')) { 15 | group.displayName = group.displayName.split(' - ').slice(3).join(' - '); 16 | } 17 | 18 | const groupEntity: GroupEntityV1alpha1 | undefined = 19 | await defaultGroupTransformer(group, groupPhoto); 20 | 21 | if (groupEntity === undefined) { 22 | return undefined; 23 | } 24 | if (groupType !== undefined && groupType.toLowerCase().includes('role')) { 25 | groupEntity.spec.type = 'role'; 26 | } 27 | if ( 28 | groupType !== undefined && 29 | groupType.toLowerCase().includes('business unit') 30 | ) { 31 | groupEntity.spec.type = 'business unit'; 32 | } 33 | if ( 34 | groupType !== undefined && 35 | groupType.toLowerCase().includes('product area') 36 | ) { 37 | groupEntity.spec.type = 'product area'; 38 | } 39 | 40 | return groupEntity; 41 | } 42 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useConfigureSlackNotificationsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | import { useConfig } from './getConfig'; 3 | import { MetricTypes } from '../utils/MetricTypes'; 4 | import { getAuthenticationTokens } from '../utils/authenticationUtils'; 5 | import { put } from '../api/client'; 6 | 7 | type SlackNotificationPayload = { 8 | teamName: string; 9 | channelId: string; 10 | componentNames: string[]; 11 | severity?: string[]; 12 | }; 13 | 14 | export const useConfigureSlackNotificationsQuery = () => { 15 | const { config, backstageAuthApi, microsoftAuthApi, endpointUrl } = useConfig( 16 | MetricTypes.configureNotifications, 17 | ); 18 | 19 | return useMutation({ 20 | mutationFn: async ({ teamName, componentNames, channelId, severity }) => { 21 | const { entraIdToken, backstageToken } = await getAuthenticationTokens( 22 | config, 23 | backstageAuthApi, 24 | microsoftAuthApi, 25 | ); 26 | return put< 27 | { 28 | teamName: string; 29 | componentNames: string[]; 30 | channelId: string; 31 | entraIdToken: string; 32 | severity?: string[]; 33 | }, 34 | any 35 | >(endpointUrl, backstageToken, { 36 | teamName, 37 | componentNames, 38 | channelId, 39 | entraIdToken, 40 | severity, 41 | }); 42 | }, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/PluginRoot.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import { CacheProvider } from '@emotion/react'; 4 | import createCache from '@emotion/cache'; 5 | import { AppContext } from './components/AppContext'; 6 | import { MetricsPlugin } from './MetricsPlugin'; 7 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 8 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 9 | 10 | const emotionInsertionPoint = document.createElement('meta'); 11 | emotionInsertionPoint.setAttribute('name', 'emotion-insertion-point'); 12 | document.querySelector('head')?.appendChild(emotionInsertionPoint); 13 | 14 | const cache = createCache({ 15 | key: 'css', 16 | insertionPoint: emotionInsertionPoint, 17 | }); 18 | 19 | const queryClient = new QueryClient(); 20 | 21 | const ProvidedPlugin = () => { 22 | const [isUsingNewFeature, setIsUsingNewFeature] = useState(false); 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const PluginRoot = () => ( 36 | 37 | } /> 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/Errors.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { Left } from './Either'; 3 | 4 | export enum OurOwnErrorMessages { 5 | UNKNOWN_ERROR = 'Vi har fått en ny API-feil som vi ikke har håndtert enda', 6 | } 7 | 8 | export type ApiError = { 9 | statusCode: StatusCodes; 10 | frontendMessage?: OurOwnErrorMessages; 11 | error?: any; 12 | }; 13 | 14 | export function errorHandling(response: Response) { 15 | switch (response.status) { 16 | case 400: { 17 | return Left.create({ 18 | statusCode: StatusCodes.BAD_REQUEST, 19 | }); 20 | } 21 | case 401: { 22 | return Left.create({ 23 | statusCode: StatusCodes.UNAUTHORIZED, 24 | }); 25 | } 26 | case 403: { 27 | return Left.create({ 28 | statusCode: StatusCodes.FORBIDDEN, 29 | }); 30 | } 31 | case 404: { 32 | return Left.create({ 33 | statusCode: StatusCodes.NOT_FOUND, 34 | }); 35 | } 36 | case 500: { 37 | return Left.create({ 38 | statusCode: StatusCodes.INTERNAL_SERVER_ERROR, 39 | }); 40 | } 41 | case 503: { 42 | return Left.create({ 43 | statusCode: StatusCodes.SERVICE_UNAVAILABLE, 44 | }); 45 | } 46 | default: { 47 | return Left.create({ 48 | statusCode: response.status, 49 | frontendMessage: OurOwnErrorMessages.UNKNOWN_ERROR, 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScmIntegrationsApi, 3 | scmIntegrationsApiRef, 4 | ScmAuth, 5 | } from '@backstage/integration-react'; 6 | import { 7 | AnyApiFactory, 8 | configApiRef, 9 | createApiFactory, 10 | storageApiRef, 11 | identityApiRef, 12 | } from '@backstage/core-plugin-api'; 13 | import { VisitsStorageApi, visitsApiRef } from '@backstage/plugin-home'; 14 | import { 15 | catalogApiRef, 16 | entityPresentationApiRef, 17 | } from '@backstage/plugin-catalog-react'; 18 | import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; 19 | import MuiAssignmentIcon from '@material-ui/icons/Assignment'; 20 | 21 | export const apis: AnyApiFactory[] = [ 22 | createApiFactory({ 23 | api: scmIntegrationsApiRef, 24 | deps: { configApi: configApiRef }, 25 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 26 | }), 27 | createApiFactory({ 28 | api: visitsApiRef, 29 | deps: { 30 | storageApi: storageApiRef, 31 | identityApi: identityApiRef, 32 | }, 33 | factory: ({ storageApi, identityApi }) => 34 | VisitsStorageApi.create({ storageApi, identityApi }), 35 | }), 36 | createApiFactory({ 37 | api: entityPresentationApiRef, 38 | deps: { 39 | catalogApi: catalogApiRef, 40 | }, 41 | factory: ({ catalogApi }) => 42 | DefaultEntityPresentationApi.create({ 43 | catalogApi, 44 | kindIcons: { 45 | function: MuiAssignmentIcon, 46 | }, 47 | }), 48 | }), 49 | ScmAuth.createDefaultApiFactory(), 50 | ]; 51 | -------------------------------------------------------------------------------- /plugins/security-champion/src/utils/authenticationUtils.ts: -------------------------------------------------------------------------------- 1 | import { IdentityApi, OAuthApi } from '@backstage/core-plugin-api'; 2 | import { Config } from '@backstage/config'; 3 | 4 | export async function getAuthenticationTokens( 5 | config: Config, 6 | backstageAuthApi: IdentityApi, 7 | microsoftAuthApi: OAuthApi, 8 | ): Promise<{ entraIdToken: string; backstageToken: string }> { 9 | const environment = config.getString('auth.environment'); 10 | const clientId = config.getString( 11 | `auth.providers.microsoft.${environment}.clientId`, 12 | ); 13 | 14 | const backstageToken = (await backstageAuthApi.getCredentials()).token; 15 | if (!backstageToken) { 16 | throw new Error('Backstage token could not be retrieved.'); 17 | } 18 | 19 | let entraIdToken: string; 20 | try { 21 | const token = await microsoftAuthApi.getAccessToken(`${clientId}/.default`); 22 | if (!token) { 23 | throw new Error('Entra ID token is undefined.'); 24 | } 25 | entraIdToken = token; 26 | } catch (error) { 27 | throw new Error( 28 | `Failed to get Entra ID token: ${error instanceof Error ? error.message : String(error)}`, 29 | ); 30 | } 31 | 32 | return { entraIdToken, backstageToken }; 33 | } 34 | 35 | export async function getBackstageToken( 36 | backstageAuthApi: IdentityApi, 37 | ): Promise<{ backstageToken: string }> { 38 | const backstageToken = (await backstageAuthApi.getCredentials()).token; 39 | if (!backstageToken) { 40 | throw new Error('Backstage token could not be retrieved.'); 41 | } 42 | 43 | return { backstageToken }; 44 | } 45 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/SuccessMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; 2 | import { Card, Box, Flex } from '@backstage/ui'; 3 | import Alert from '@mui/material/Alert'; 4 | import Link from '@mui/material/Link'; 5 | import { catalogCreatorTranslationRef } from '../../utils/translations'; 6 | 7 | import style from '../../catalog.module.css'; 8 | 9 | interface SuccessMessageProps { 10 | prUrl?: string; 11 | onReset: () => void; 12 | } 13 | 14 | export const SuccessMessage = ({ prUrl, onReset }: SuccessMessageProps) => { 15 | const { t } = useTranslationRef(catalogCreatorTranslationRef); 16 | return ( 17 | 18 | 19 | 24 | 25 | {t('successPage.successfullyCreatedPR')}:{' '} 26 | {prUrl ? ( 27 | 33 | {prUrl} 34 | 35 | ) : ( 36 |

{t('successPage.couldNotRetrieveURL')}

37 | )} 38 |
39 | 40 | {t('successPage.registerNew')} 41 | 42 |
43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/RepositoriesTable/RepositoryScannerStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/material/styles'; 2 | import Typography from '@mui/material/Typography'; 3 | import { Stack } from '@mui/system'; 4 | import { RepositorySummary } from '../../typesFrontend'; 5 | import { getScannersGroupedByStatus } from './utils'; 6 | import CheckIcon from '@mui/icons-material/Check'; 7 | import CloseIcon from '@mui/icons-material/Close'; 8 | 9 | type Props = { 10 | repository: RepositorySummary; 11 | }; 12 | 13 | export const RepositoryScannerStatus = ({ repository }: Props) => { 14 | const theme = useTheme(); 15 | const { configured, notConfigured } = getScannersGroupedByStatus(repository); 16 | const noScannerConfigured = configured.length === 0; 17 | 18 | return ( 19 | 20 | {configured.length === 4 ? ( 21 | 27 | 28 | Alle scannere aktivert 29 | 30 | ) : ( 31 | 38 | 39 | {noScannerConfigured 40 | ? 'Ingen aktiverte scannere' 41 | : notConfigured.join(', ')} 42 | 43 | )} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/NoAccessAlert.tsx: -------------------------------------------------------------------------------- 1 | import { OpenInFull } from '@mui/icons-material'; 2 | import Alert from '@mui/material/Alert'; 3 | import AlertTitle from '@mui/material/AlertTitle'; 4 | import Dialog from '@mui/material/Dialog'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import IconButton from '@mui/material/IconButton'; 8 | import { useState } from 'react'; 9 | 10 | export interface NoAccessProps { 11 | repos: string[]; 12 | } 13 | 14 | const NoAccessAlert = ({ repos }: NoAccessProps) => { 15 | const [openDialog, setOpenDialog] = useState(false); 16 | 17 | const openDialogBox = () => { 18 | setOpenDialog(true); 19 | }; 20 | 21 | const closeDialogBox = () => { 22 | setOpenDialog(false); 23 | }; 24 | 25 | return ( 26 | = 1 && ( 30 | } 32 | color="inherit" 33 | onClick={openDialogBox} 34 | /> 35 | ) 36 | } 37 | > 38 | 39 | Du mangler tilgang til {repos.length} 40 | {` komponent${repos.length > 1 ? 'er' : ''}`} 41 | 42 | 43 | Komponenter du mangler tilgang til 44 | 45 | {repos.map(repo => ( 46 |
{repo}
47 | ))} 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default NoAccessAlert; 55 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/RosStatus/utils.ts: -------------------------------------------------------------------------------- 1 | import { BASIC_COLORS, SEVERITY_COLORS } from '../../colors'; 2 | import { RosStatus } from '../../typesFrontend'; 3 | 4 | export const colorMap: Record = { 5 | Unknown: BASIC_COLORS.GREY, 6 | VeryOutdated: SEVERITY_COLORS.CRITICAL, 7 | Outdated: SEVERITY_COLORS.HIGH, 8 | SomewhatOutdated: SEVERITY_COLORS.MEDIUM, 9 | Recent: BASIC_COLORS.SUCCESS, 10 | }; 11 | 12 | export const labelMap: Record = { 13 | Unknown: 'Ukjent status', 14 | VeryOutdated: 'Veldig utdatert', 15 | Outdated: 'Utdatert', 16 | SomewhatOutdated: 'Noe utdatert', 17 | Recent: 'Nylig oppdatert', 18 | }; 19 | 20 | export const calculateDaysSince = (lastPublishedRisc: string): number => { 21 | const now = new Date(); 22 | const rosDate = new Date(lastPublishedRisc); 23 | return Math.ceil((now.getTime() - rosDate.getTime()) / (1000 * 60 * 60 * 24)); 24 | }; 25 | 26 | export const getDaysColor = (days: number, isDarkMode: boolean) => { 27 | if (days <= 30) 28 | return isDarkMode ? BASIC_COLORS.WHITE : BASIC_COLORS.DARK_GREY; 29 | if (days <= 90) return colorMap.SomewhatOutdated; 30 | if (days <= 180) return colorMap.Outdated; 31 | return colorMap.VeryOutdated; 32 | }; 33 | 34 | export const getCommitsColor = (commits: number, isDarkMode: boolean) => { 35 | if (commits <= 10) 36 | return isDarkMode ? BASIC_COLORS.WHITE : BASIC_COLORS.DARK_GREY; 37 | if (commits <= 25) return colorMap.SomewhatOutdated; 38 | if (commits <= 50) return colorMap.Outdated; 39 | return colorMap.VeryOutdated; 40 | }; 41 | 42 | export const plural = (n: number, one: string, many: string) => 43 | n === 1 ? one : many; 44 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/mapping/getScannerData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregatedScannerStatus, 3 | Repository, 4 | RepositoryScannerStatusData, 5 | RepositorySummary, 6 | Scanner, 7 | } from '../typesFrontend'; 8 | 9 | const getScannerBooleans = (r: Repository | RepositorySummary) => ({ 10 | dependabot: r.scannerConfig.dependabot, 11 | codeql: r.scannerConfig.codeQL, 12 | pharos: r.scannerConfig.pharos, 13 | sysdig: r.scannerConfig.sysdig, 14 | }); 15 | 16 | export const getScannerStatusData = ( 17 | input: Repository | RepositorySummary[], 18 | ): RepositoryScannerStatusData[] => { 19 | const items = Array.isArray(input) ? input : [input]; 20 | return items.map(r => { 21 | const s = getScannerBooleans(r); 22 | return { 23 | componentName: r.componentName, 24 | scannerStatus: [ 25 | { type: Scanner.Dependabot, on: s.dependabot }, 26 | { type: Scanner.CodeQL, on: s.codeql }, 27 | { type: Scanner.Pharos, on: s.pharos }, 28 | { type: Scanner.Sysdig, on: s.sysdig }, 29 | ], 30 | }; 31 | }); 32 | }; 33 | 34 | export const getAggregatedScannerStatus = ( 35 | rows: RepositoryScannerStatusData[], 36 | ): AggregatedScannerStatus[] => { 37 | const scanners = Object.values(Scanner); 38 | 39 | return scanners.map(scanner => { 40 | let on = 0; 41 | let off = 0; 42 | for (const repo of rows) { 43 | const s = repo.scannerStatus.find(x => x.type === scanner); 44 | if (s) { 45 | if (s.on) on++; 46 | else off++; 47 | } 48 | } 49 | return { 50 | scannerName: scanner, 51 | status: `${on}/${on + off}`, 52 | repositoryStatus: rows, 53 | }; 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/utils/getCatalogInfo.ts: -------------------------------------------------------------------------------- 1 | import { OAuthApi } from '@backstage/core-plugin-api'; 2 | import { Octokit } from '@octokit/rest'; 3 | import * as yaml from 'yaml'; 4 | import { RequiredYamlFields } from '../types/types'; 5 | 6 | export async function getCatalogInfo( 7 | url: string, 8 | githubAuthApi: OAuthApi, 9 | ): Promise { 10 | const match = url.match( 11 | /github\.com\/([^\/]+)\/([^\/]+)\/(?:blob|tree)\/([^\/]+)\/(.+)/, 12 | ); 13 | 14 | if (!match) { 15 | throw new Error('Invalid GitHub repository URL'); 16 | } 17 | 18 | const owner = match[1]; 19 | const repo = match[2]; 20 | const ref = match[3]; 21 | const path = match[4]; 22 | 23 | try { 24 | const octokit = new Octokit({ 25 | auth: await githubAuthApi.getAccessToken(), 26 | }); 27 | 28 | const response = await octokit.rest.repos.getContent({ 29 | owner: owner, 30 | repo: repo, 31 | path: path, 32 | ref: ref, 33 | headers: { 34 | 'If-None-Match': '', 35 | }, 36 | }); 37 | 38 | const fileContent = Buffer.from( 39 | (response.data as { content: string }).content, 40 | 'base64', 41 | ).toString('utf8'); 42 | const parsedYaml = yaml.parseAllDocuments(fileContent); 43 | const documentList: Array = parsedYaml.map(document => { 44 | return document.toJSON(); 45 | }); 46 | return documentList; 47 | } catch (error: unknown) { 48 | if (error instanceof Error) { 49 | if (error.message.toLowerCase().includes('credentials')) { 50 | throw error; 51 | } 52 | return null; 53 | } 54 | throw error; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/README.md: -------------------------------------------------------------------------------- 1 | # Security metrics (backend-plugin) 2 | 3 | This is a backend plugin for the Security metrics Backstage plugin. The backend plugin works as a proxy for the frontend 4 | plugin, and allows the system to communicate with the security metrics API. The backend plugin has two roles: 5 | 6 | - Handle the on-behalf-of flow to acquire JWT for the backend API 7 | - Provide the user with a scope to aquire the Entra ID JWT that was used to log into backstage 8 | 9 | ### Kartverket.dev configuration 10 | 11 | > **_NOTE:_** Ensure that you have installed 12 | > the [frontend plugin](https://www.npmjs.com/package/@kartverket/backstage-plugin-security-metrics-frontend) aswell 13 | > 14 | > In `app-config.production.yaml` add the following under the `sikkerhetsmetrikker` config-block: 15 | 16 | ```yaml 17 | clientId: ${SMAPI_CLIENT_ID} 18 | baseUrl: http://sikkerhetsmetrikker.sikkerhetsmetrikker-main:8080/api 19 | ``` 20 | 21 | In `packages/backend/src/index.ts` add the following line in order add the backend plugin: 22 | 23 | ```typescript 24 | // Security metrics 25 | backend.add(import("@kartverket/backstage-plugin-security-metrics-backend")) 26 | ``` 27 | 28 | and in `packages/backend/package.json` add the following dependency. 29 | 30 | ```json 31 | "@kartverket/backstage-plugin-security-metrics-backend": "^1.0.0" 32 | ``` 33 | 34 | It may be better to use `yarn add @kartverket/backstage-plugin-security-metrics-backend` from the `packages/backend` 35 | directory 36 | 37 | ### Links 38 | 39 | - [Github repository for both plugins](https://github.com/kartverket/sikkerhetsmetrikker-plugin) 40 | - [Github repository for backend](https://github.com/kartverket/sikkerhetsmetrikker-plugin-backend) 41 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useStarredRefFilter.ts: -------------------------------------------------------------------------------- 1 | import { useApi, storageApiRef } from '@backstage/core-plugin-api'; 2 | import { useEffect, useMemo, useState } from 'react'; 3 | import { FilterEnum } from '../typesFrontend'; 4 | 5 | export const useStarredRefFilter = ({ 6 | allRefs, 7 | starredEntities, 8 | storageKey = 'filterChoice', 9 | bucket = 'group-page-filter', 10 | }: { 11 | allRefs: string[]; 12 | starredEntities: Set; 13 | storageKey?: string; 14 | bucket?: string; 15 | }) => { 16 | const storageApi = useApi(storageApiRef); 17 | const storage = storageApi.forBucket(bucket); 18 | 19 | const starredRefs = useMemo( 20 | () => allRefs.filter(ref => starredEntities.has(ref)), 21 | [allRefs, starredEntities], 22 | ); 23 | 24 | const hasStarred = starredRefs.length > 0; 25 | 26 | const getInitialFilter = (): FilterEnum => { 27 | const snap = storage.snapshot(storageKey); 28 | 29 | if (snap?.presence === 'present' && snap.value) { 30 | return snap.value; 31 | } 32 | 33 | return 'all'; 34 | }; 35 | 36 | const [filterChoice, setFilterChoice] = 37 | useState(getInitialFilter); 38 | 39 | const effectiveFilter: FilterEnum = 40 | hasStarred && filterChoice === 'starred' ? 'starred' : 'all'; 41 | 42 | const visibleRefs = useMemo( 43 | () => new Set(effectiveFilter === 'starred' ? starredRefs : allRefs), 44 | [effectiveFilter, allRefs, starredRefs], 45 | ); 46 | 47 | useEffect(() => { 48 | storage.set(storageKey, filterChoice); 49 | }, [filterChoice, storage, storageKey]); 50 | 51 | return { 52 | hasStarred, 53 | effectiveFilter, 54 | visibleRefs, 55 | setFilterChoice, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kartverket/backstage-plugin-security-metrics-backend", 3 | "version": "3.18.1", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "publishConfig": { 8 | "access": "public", 9 | "main": "dist/index.cjs.js", 10 | "types": "dist/index.d.ts" 11 | }, 12 | "backstage": { 13 | "role": "backend-plugin", 14 | "pluginId": "security-metrics", 15 | "pluginPackages": [ 16 | "@kartverket/backstage-plugin-security-metrics-backend" 17 | ] 18 | }, 19 | "scripts": { 20 | "start": "backstage-cli package start", 21 | "build": "backstage-cli package build", 22 | "lint": "backstage-cli package lint", 23 | "clean": "backstage-cli package clean", 24 | "prepack": "backstage-cli package prepack", 25 | "postpack": "backstage-cli package postpack", 26 | "publishToNpm": "npm publish", 27 | "tsc": "tsc" 28 | }, 29 | "dependencies": { 30 | "@azure/msal-node": "^2.10.0", 31 | "@backstage/backend-defaults": "backstage:^", 32 | "@backstage/backend-plugin-api": "backstage:^", 33 | "@backstage/config": "backstage:^", 34 | "@backstage/core-plugin-api": "backstage:^", 35 | "@types/express": "^4.17.12", 36 | "express": "^4.21.1", 37 | "express-promise-router": "^4.1.1", 38 | "http-status-codes": "^2.3.0", 39 | "node-fetch": "^3.3.2", 40 | "winston": "^3.15.0", 41 | "yn": "^5.0.0" 42 | }, 43 | "devDependencies": { 44 | "@backstage/cli": "backstage:^", 45 | "@backstage/plugin-auth-backend": "backstage:^", 46 | "@backstage/plugin-auth-backend-module-guest-provider": "backstage:^", 47 | "msw": "^2.5.1" 48 | }, 49 | "files": [ 50 | "dist" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /plugins/security-metrics-backend/src/services/EntraIdService/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { IEntraIdService } from '../../contracts/IEntraIdService'; 2 | import { 3 | ConfidentialClientApplication, 4 | Configuration, 5 | OnBehalfOfRequest, 6 | } from '@azure/msal-node'; 7 | import { LoggerService } from '@backstage/backend-plugin-api'; 8 | import { EntraIdConfig } from '../config'; 9 | 10 | export class EntraIdService implements IEntraIdService { 11 | private entraIdConfig: EntraIdConfig; 12 | private azureAdClient: ConfidentialClientApplication; 13 | private logger: LoggerService; 14 | 15 | constructor(entraIdConfig: EntraIdConfig, logger: LoggerService) { 16 | this.logger = logger; 17 | this.entraIdConfig = entraIdConfig; 18 | const msalConfig: Configuration = { 19 | auth: { 20 | clientId: this.entraIdConfig.clientId, 21 | clientSecret: this.entraIdConfig.clientSecret, 22 | authority: `https://login.microsoftonline.com/${this.entraIdConfig.tenantId}`, 23 | }, 24 | }; 25 | this.azureAdClient = new ConfidentialClientApplication(msalConfig); 26 | } 27 | 28 | async getOboToken(token: string): Promise { 29 | try { 30 | const oboRequest: OnBehalfOfRequest = { 31 | oboAssertion: token, 32 | scopes: [this.entraIdConfig.scope], 33 | }; 34 | 35 | const response = 36 | await this.azureAdClient.acquireTokenOnBehalfOf(oboRequest); 37 | 38 | return response?.accessToken; 39 | } catch (error) { 40 | // @ts-ignore 41 | this.logger.error(error.message); 42 | throw new Error( 43 | `OBO Token acquisition failed: ${ 44 | error instanceof Error ? error.message : 'Unknown error' 45 | }`, 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/Trend/Trend.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { getFromDate } from '../utils'; 3 | import { ErrorBanner } from '../ErrorBanner'; 4 | import { useTrendsQuery } from '../../hooks/useTrendsQuery'; 5 | import Typography from '@mui/material/Typography'; 6 | import { CardTitle } from '../CardTitle'; 7 | import { Graph } from './TrendGraph'; 8 | import { GraphLabels } from './GraphLabels'; 9 | import { Progress } from '@backstage/core-components'; 10 | 11 | interface TrendProps { 12 | componentNames: string[] | string; 13 | } 14 | 15 | export const Trend = ({ componentNames }: TrendProps) => { 16 | const toDate = useRef(new Date()).current; 17 | const [fromDate, setFromDate] = useState(() => 18 | getFromDate('oneMonth', toDate), 19 | ); 20 | const [graphTimeline, setGraphTimeline] = useState('oneMonth'); 21 | 22 | const items = Array.isArray(componentNames) 23 | ? componentNames 24 | : [componentNames]; 25 | 26 | const { data, isPending, error } = useTrendsQuery(items, fromDate, toDate); 27 | 28 | return ( 29 | 30 | {isPending && } 31 | {data?.length === 0 && ( 32 | 33 | Vi fant dessverre ingen historiske data. 34 | 35 | )} 36 | {data && ( 37 | <> 38 | 39 | 44 | 45 | )} 46 | {error && } 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | image: postgres:15 6 | container_name: backstage-postgres 7 | environment: 8 | POSTGRES_USER: admin # Admin user for PostgreSQL 9 | POSTGRES_PASSWORD: adminpw # Admin user password 10 | POSTGRES_DB: backstage # Database name for Backstage 11 | volumes: 12 | - postgres_data:/var/lib/postgresql/data 13 | ports: 14 | - '5432:5432' # Expose PostgreSQL on port 5432 15 | networks: 16 | - backstage-network 17 | 18 | backstage: 19 | build: 20 | context: . 21 | dockerfile: packages/backend/Dockerfile 22 | environment: 23 | APP_CONFIG_app_baseUrl: 'http://localhost:7007' 24 | APP_CONFIG_backend_baseUrl: 'http://localhost:7007' 25 | APP_CONFIG_backend_database_client: 'pg' 26 | APP_CONFIG_backend_database_connection_host: 'postgres' 27 | APP_CONFIG_backend_database_connection_user: 'admin' 28 | APP_CONFIG_backend_database_connection_password: 'adminpw' 29 | APP_CONFIG_backend_database_connection_database: 'backstage' 30 | APP_CONFIG_backend_cors_origin: 'http://localhost:3000' 31 | ENTRA_ID_SP_APP_ID: 432 32 | ENTRA_ID_SP_CLIENT_SECRET: 432 33 | ENTRA_WEB_APP_ID: 432 34 | ENTRA_WEB_CLIENT_SECRET: 432 35 | ENTRA_TENANT_ID: 432 36 | GOOGLE_OAUTH_CLIENT_ID: 432 37 | GOOGLE_OAUTH_CLIENT_SECRET: 432 38 | ports: 39 | - '7007:7007' # Expose Backstage on port 7007 40 | depends_on: 41 | - postgres 42 | networks: 43 | - backstage-network 44 | volumes: 45 | - ./github-secret.yaml:/app/github-secrets/github-app-backstage-skip-credentials.yaml 46 | 47 | volumes: 48 | postgres_data: {} 49 | 50 | networks: 51 | backstage-network: 52 | driver: bridge 53 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useAcceptVulnerabilityQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { getAuthenticationTokens } from '../utils/authenticationUtils'; 3 | import { MetricTypes } from '../utils/MetricTypes'; 4 | import { useConfig } from './getConfig'; 5 | import { post } from '../api/client'; 6 | import { componentMetricsQueryKeys } from './useComponentMetricsQuery'; 7 | 8 | type AcceptVulnVariables = { 9 | componentName: string; 10 | vulnerabilityId: string; 11 | comment?: string; 12 | acceptedBy?: string; 13 | }; 14 | 15 | export const useAcceptVulnerabilityQuery = (repoName: string) => { 16 | const queryClient = useQueryClient(); 17 | const { config, backstageAuthApi, microsoftAuthApi, endpointUrl } = useConfig( 18 | MetricTypes.acceptVulnerability, 19 | ); 20 | 21 | return useMutation({ 22 | mutationFn: async ({ 23 | componentName, 24 | vulnerabilityId, 25 | comment, 26 | acceptedBy, 27 | }) => { 28 | const { entraIdToken, backstageToken } = await getAuthenticationTokens( 29 | config, 30 | backstageAuthApi, 31 | microsoftAuthApi, 32 | ); 33 | return post< 34 | { 35 | componentName: string; 36 | vulnerabilityId: string; 37 | comment?: string; 38 | acceptedBy?: string; 39 | entraIdToken: string; 40 | }, 41 | any 42 | >(endpointUrl, backstageToken, { 43 | componentName, 44 | vulnerabilityId, 45 | comment, 46 | acceptedBy, 47 | entraIdToken, 48 | }); 49 | }, 50 | onSettled: () => { 51 | return queryClient.invalidateQueries({ 52 | queryKey: componentMetricsQueryKeys.metrics(repoName), 53 | }); 54 | }, 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/hooks/useUpdateDependentFormFields.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@backstage/catalog-model'; 2 | import { useEffect } from 'react'; 3 | import { FieldPath, UseFormSetValue } from 'react-hook-form'; 4 | import z from 'zod/v4'; 5 | import { formSchema } from '../schemas/formSchema'; 6 | 7 | export const useUpdateDependentFormFields = ( 8 | options: Entity[], 9 | valueToWatch: string[] | undefined, 10 | fieldPath: FieldPath>, 11 | setValue: UseFormSetValue>, 12 | ) => { 13 | const formatEntityString = (entity: Entity): string => { 14 | return `${entity.kind.toLowerCase()}:${entity.metadata.namespace?.toLowerCase() ?? 'default'}/${entity.metadata.name}`; 15 | }; 16 | 17 | useEffect(() => { 18 | if ( 19 | valueToWatch && 20 | valueToWatch.length > 0 && 21 | valueToWatch[0] !== '' && 22 | options.length > 0 23 | ) { 24 | const intersection = options.flatMap(value => { 25 | if (valueToWatch.includes(value.metadata.name)) { 26 | return value.metadata.name; 27 | } 28 | if (valueToWatch.includes(formatEntityString(value))) { 29 | return formatEntityString(value); 30 | } 31 | return []; 32 | }); 33 | const elementsToDelete = [ 34 | ...valueToWatch.filter(e => !intersection.includes(e)), 35 | ]; 36 | if (elementsToDelete.length > 0) { 37 | if ( 38 | valueToWatch.length === 1 && 39 | intersection.includes(valueToWatch[0]) 40 | ) { 41 | setValue(fieldPath, valueToWatch[0]); 42 | } else { 43 | setValue(fieldPath, [ 44 | ...valueToWatch.filter(e => intersection.includes(e)), 45 | ]); 46 | } 47 | } 48 | } 49 | }, [valueToWatch, options, fieldPath, setValue]); 50 | }; 51 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/usePagination.ts: -------------------------------------------------------------------------------- 1 | import { TablePaginationProps } from '@mui/material/TablePagination'; 2 | // eslint-disable-next-line no-restricted-imports 3 | import TablePaginationActions from '@mui/material/TablePagination/TablePaginationActions'; 4 | import { ChangeEvent, useState } from 'react'; 5 | import type { MouseEvent as ReactMouseEvent } from 'react'; 6 | 7 | /** 8 | * Hook for handling pagination props for a MUI TablePagination component. 9 | * @param count The total number of items to paginate 10 | * @param initialPage The initial page to start on 11 | * @param initialRowsPerPage The number of rows per page to start with 12 | * @param rowsPerPageOptions The options for the rows per page select 13 | */ 14 | export const usePaginationProps = ( 15 | count: number, 16 | initialPage: number = 0, 17 | initialRowsPerPage: number = 10, 18 | rowsPerPageOptions: number[] = [5, 10, 25, 50], 19 | ): TablePaginationProps => { 20 | const [page, setPage] = useState(initialPage); 21 | const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage); 22 | 23 | const onPageChange = ( 24 | _event: ReactMouseEvent | null, 25 | newPage: number, 26 | ) => { 27 | setPage(newPage); 28 | }; 29 | 30 | const onRowsPerPageChange = ( 31 | event: ChangeEvent, 32 | ) => { 33 | setRowsPerPage(parseInt(event.target.value, 10)); 34 | setPage(0); 35 | }; 36 | 37 | const slotProps = { 38 | select: { 39 | inputProps: { 40 | 'aria-label': 'Rader per side', 41 | }, 42 | native: true, 43 | }, 44 | }; 45 | return { 46 | count, 47 | page, 48 | rowsPerPage, 49 | onPageChange, 50 | onRowsPerPageChange, 51 | rowsPerPageOptions: rowsPerPageOptions, 52 | ActionsComponent: TablePaginationActions, 53 | slotProps: slotProps, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/mapping/getSeverityCounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SeverityCount, 3 | SikkerhetsmetrikkerSystemTotal, 4 | } from '../typesFrontend'; 5 | 6 | export const aggregateSeverityCounts = ( 7 | items: T[], 8 | getSeverityObject: (item: T) => Partial, 9 | ): SeverityCount => { 10 | return items.reduce( 11 | (totals, item) => { 12 | const counts = getSeverityObject(item); 13 | Object.keys(totals).forEach(key => { 14 | const severityKey = key as keyof SeverityCount; 15 | totals[severityKey] += counts[severityKey] || 0; 16 | }); 17 | return totals; 18 | }, 19 | { 20 | critical: 0, 21 | high: 0, 22 | medium: 0, 23 | low: 0, 24 | negligible: 0, 25 | unknown: 0, 26 | }, 27 | ); 28 | }; 29 | 30 | export const getTotalVulnerabilityCount = (severityCount: SeverityCount) => { 31 | return ( 32 | severityCount.critical + 33 | severityCount.high + 34 | severityCount.medium + 35 | severityCount.low + 36 | severityCount.negligible + 37 | severityCount.unknown 38 | ); 39 | }; 40 | 41 | export const getSeverityCountPerSystem = ( 42 | data: SikkerhetsmetrikkerSystemTotal[], 43 | ): { systemName: string; severityCount: SeverityCount }[] => 44 | data.map(s => { 45 | const total: SeverityCount = { 46 | unknown: 0, 47 | negligible: 0, 48 | low: 0, 49 | medium: 0, 50 | high: 0, 51 | critical: 0, 52 | }; 53 | 54 | for (const repo of s.metrics?.permittedMetrics ?? []) { 55 | const sc = repo.severityCount; 56 | total.unknown += sc.unknown; 57 | total.negligible += sc.negligible; 58 | total.low += sc.low; 59 | total.medium += sc.medium; 60 | total.high += sc.high; 61 | total.critical += sc.critical; 62 | } 63 | 64 | return { systemName: s.systemName, severityCount: total }; 65 | }); 66 | -------------------------------------------------------------------------------- /plugins/security-champion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kartverket/backstage-plugin-security-champion", 3 | "version": "0.1.8", 4 | "license": "Apache-2.0", 5 | "main": "src/index.ts", 6 | "types": "src/index.ts", 7 | "publishConfig": { 8 | "access": "public", 9 | "main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts" 11 | }, 12 | "backstage": { 13 | "role": "frontend-plugin", 14 | "pluginId": "security-champion", 15 | "pluginPackages": [ 16 | "@kartverket/backstage-plugin-security-champion" 17 | ] 18 | }, 19 | "sideEffects": false, 20 | "scripts": { 21 | "start": "backstage-cli package start", 22 | "build": "backstage-cli package build", 23 | "lint": "backstage-cli package lint", 24 | "clean": "backstage-cli package clean", 25 | "prepack": "backstage-cli package prepack", 26 | "postpack": "backstage-cli package postpack", 27 | "tsc": "tsc", 28 | "publishToNpm": "npm publish" 29 | }, 30 | "dependencies": { 31 | "@backstage/catalog-model": "backstage:^", 32 | "@backstage/config": "backstage:^", 33 | "@backstage/core-components": "backstage:^", 34 | "@backstage/core-plugin-api": "backstage:^", 35 | "@backstage/plugin-catalog-react": "backstage:^", 36 | "@backstage/theme": "backstage:^", 37 | "@backstage/ui": "backstage:^", 38 | "@mui/icons-material": "^7.3.5", 39 | "@tanstack/react-query": "^5.84.1", 40 | "react-use": "^17.2.4" 41 | }, 42 | "peerDependencies": { 43 | "@mui/material": "5.16.4", 44 | "@mui/system": "5.16.4", 45 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0" 46 | }, 47 | "devDependencies": { 48 | "@backstage/cli": "backstage:^", 49 | "@backstage/core-app-api": "backstage:^", 50 | "@backstage/dev-utils": "backstage:^", 51 | "msw": "^1.0.0", 52 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0" 53 | }, 54 | "files": [ 55 | "dist" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/SysdigContent.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | import { Vulnerability } from '../../../typesFrontend'; 4 | import { Link } from '@backstage/core-components'; 5 | import { ErrorBanner } from '../../ErrorBanner'; 6 | import { IdsWithUrls } from './IdsWithUrls'; 7 | 8 | interface SysdigContentProps { 9 | vulnerability: Vulnerability; 10 | } 11 | 12 | export const SysdigContent = ({ vulnerability }: SysdigContentProps) => { 13 | const info = vulnerability.scannerSpecificInfo.sysdigInfo; 14 | if (!info) { 15 | return ; 16 | } 17 | 18 | const link = new URL(info.htmlUrl).href; 19 | 20 | const clusterToNamespaces = info.locations.reduce< 21 | Record> 22 | >((acc, location) => { 23 | if (!acc[location.cluster]) acc[location.cluster] = new Set(); 24 | if (location.namespace) acc[location.cluster]!.add(location.namespace); 25 | return acc; 26 | }, {}); 27 | 28 | return ( 29 | 30 | 31 |
32 | Sysdig-link: 33 | 34 | {link.length > 80 ? link.slice(0, 30).concat('...') : link} 35 | 36 |
37 | Is exploitable: 38 | {info.isExploitable ? 'Yes' : 'No'} 39 |
40 | Packages: 41 | {info.packages.join(', ')} 42 |
43 | Containers: 44 | {info.containerNames.join(', ')} 45 |
46 | Locations: 47 | {Object.entries(clusterToNamespaces).map(([cluster, nsSet]) => ( 48 | 49 | {cluster}: {Array.from(nsSet).join(', ')} 50 | 51 | ))} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /plugins/function-kind-common/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common functionalities for the test-new-kind plugin. 3 | * 4 | * @packageDocumentation 5 | */ 6 | 7 | import { Entity } from '@backstage/catalog-model'; 8 | 9 | // Export schema validators 10 | export { functionEntityV1alpha1Validator, schemas } from './schema'; 11 | 12 | /** 13 | * Function entity definition - version 1 alpha 1 14 | * 15 | * This represents a function instance in your infrastructure 16 | */ 17 | export interface FunctionEntityV1alpha1 extends Entity { 18 | apiVersion: 'kartverket.dev/v1alpha1'; 19 | kind: 'Function'; 20 | spec: { 21 | /** 22 | * The type of function (e.g., '?', '??' Dette må fylles inn når vi vet hvilke typer som kan eksistere.) 23 | */ 24 | type: string; 25 | 26 | /** 27 | * The lifecycle stage of the function 28 | */ 29 | lifecycle: string; 30 | 31 | /** 32 | * The owner of the function - typically a team 33 | */ 34 | owner: string; 35 | 36 | /** 37 | * Optional: The systems this function is a parent of 38 | */ 39 | childSystems?: string[]; 40 | 41 | /** 42 | * Optional: The functions this function is a parent of 43 | */ 44 | childFunctions?: string[]; 45 | }; 46 | } 47 | 48 | /** 49 | * Type guard to check if an entity is a functionEntity 50 | */ 51 | export function isFunctionEntity( 52 | entity: Entity, 53 | ): entity is FunctionEntityV1alpha1 { 54 | return ( 55 | entity.apiVersion === 'kartverket.dev/v1alpha1' && 56 | entity.kind === 'Function' 57 | ); 58 | } 59 | 60 | /** 61 | * Constants for function entity 62 | */ 63 | export const FUNCTION_ENTITY_KIND = 'Function'; 64 | export const FUNCTION_API_VERSION = 'kartverket.dev/v1alpha1'; 65 | 66 | /** 67 | * Well-known function types 68 | */ 69 | export const FUNCTION_TYPES = {} as const; 70 | 71 | export type FunctionType = (typeof FUNCTION_TYPES)[keyof typeof FUNCTION_TYPES]; 72 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/ScannerDetails/ScannerCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/material/styles'; 2 | import Typography from '@mui/material/Typography'; 3 | import Box from '@mui/system/Box'; 4 | import { ReactNode } from 'react'; 5 | import { BASIC_COLORS, SCANNER_CARD, SCANNER_COLORS } from '../../../colors'; 6 | import { Scanner, Severity } from '../../../typesFrontend'; 7 | import { getScannerColor } from '../../utils'; 8 | 9 | type LabelProps = { 10 | color: string; 11 | title: string | Severity; 12 | }; 13 | 14 | const ScannerLabel = ({ color, title }: LabelProps): ReactNode => { 15 | let textColor; 16 | if (color === SCANNER_COLORS.SYSDIG) { 17 | textColor = BASIC_COLORS.BLACK; 18 | } else { 19 | textColor = BASIC_COLORS.WHITE; 20 | } 21 | return ( 22 | 33 | 34 | {title.toUpperCase()} 35 | 36 | 37 | ); 38 | }; 39 | 40 | type Props = { 41 | scanner: Scanner; 42 | children: ReactNode; 43 | }; 44 | 45 | export const ScannerCard = ({ scanner, children }: Props) => { 46 | const theme = useTheme(); 47 | const isDarkMode = theme.palette.mode === 'dark'; 48 | return ( 49 | 64 | 65 | {children} 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 20 | 21 | 22 | 27 | 33 | 39 | 44 | <%= config.getString('app.title') %> 45 | 46 | 47 | 48 | 49 |
50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/utils.ts: -------------------------------------------------------------------------------- 1 | import { Severity, Vulnerability } from '../../typesFrontend'; 2 | 3 | const severityOrder: Record = { 4 | ['critical']: 1, 5 | ['high']: 2, 6 | ['medium']: 3, 7 | ['low']: 4, 8 | ['negligible']: 5, 9 | ['unknown']: 6, 10 | }; 11 | 12 | export const sortableHeaders = [ 13 | 'Dato', 14 | 'Skannere', 15 | 'Alvorlighetsgrad', 16 | 'Status', 17 | ] as const; 18 | export type SortableHeader = (typeof sortableHeaders)[number]; 19 | 20 | export const sortVulnerabilities = ( 21 | a: Vulnerability, 22 | b: Vulnerability, 23 | sortType: SortableHeader, 24 | order: 'asc' | 'desc', 25 | ) => { 26 | switch (sortType) { 27 | case 'Dato': { 28 | const missing = order === 'asc' ? -Infinity : Infinity; 29 | const tA = a.dateFirstSeen 30 | ? new Date(a.dateFirstSeen).getTime() 31 | : missing; 32 | const tB = b.dateFirstSeen 33 | ? new Date(b.dateFirstSeen).getTime() 34 | : missing; 35 | return order === 'asc' ? tA - tB : tB - tA; 36 | } 37 | case 'Skannere': 38 | return order === 'asc' 39 | ? a.scanners[0].localeCompare(b.scanners[0]) 40 | : b.scanners[0].localeCompare(a.scanners[0]); 41 | case 'Alvorlighetsgrad': 42 | return order === 'asc' 43 | ? severityOrder[b.severity] - severityOrder[a.severity] 44 | : severityOrder[a.severity] - severityOrder[b.severity]; 45 | case 'Status': { 46 | const aAccepted = a.acceptedAt ? 1 : 0; 47 | const bAccepted = b.acceptedAt ? 1 : 0; 48 | if (aAccepted !== bAccepted) { 49 | return order === 'asc' ? aAccepted - bAccepted : bAccepted - aAccepted; 50 | } 51 | const sev = severityOrder[a.severity] - severityOrder[b.severity]; 52 | if (sev) return order === 'asc' ? sev : -sev; 53 | const tA = new Date(a.dateFirstSeen as any).getTime(); 54 | const tB = new Date(b.dateFirstSeen as any).getTime(); 55 | return order === 'asc' ? tA - tB : tB - tA; 56 | } 57 | default: 58 | return 0; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/ScannerStatus/SystemScannerStatuses.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getAggregatedScannerStatus, 3 | getScannerStatusData, 4 | } from '../../mapping/getScannerData'; 5 | import { 6 | AggregatedScannerStatus, 7 | RepositorySummary, 8 | } from '../../typesFrontend'; 9 | import { CardTitle } from '../CardTitle'; 10 | import { StyledTableRow } from '../TableRow'; 11 | import { ScannerStatusDialog } from './SystemStatusDialog'; 12 | import Box from '@mui/material/Box'; 13 | import Table from '@mui/material/Table'; 14 | import TableBody from '@mui/material/TableBody'; 15 | import TableCell from '@mui/material/TableCell'; 16 | import Typography from '@mui/material/Typography'; 17 | import { ScannerInfo } from './ScannerInfo'; 18 | 19 | interface SystemScannerStatusProps { 20 | data: RepositorySummary[]; 21 | } 22 | 23 | export const SystemScannerStatuses = ({ data }: SystemScannerStatusProps) => { 24 | const repositoryScannerStatus = getScannerStatusData(data); 25 | 26 | const aggregatedStatus = getAggregatedScannerStatus(repositoryScannerStatus); 27 | 28 | if (!aggregatedStatus || aggregatedStatus.length === 0) { 29 | return ( 30 | 31 | 32 | 33 | Vi fant dessverre ingen status på skannere. 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | {aggregatedStatus.map((status: AggregatedScannerStatus) => ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ))} 55 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/Trend/GraphLabels.tsx: -------------------------------------------------------------------------------- 1 | import Chip from '@mui/material/Chip'; 2 | import { useMemo } from 'react'; 3 | import { BASIC_COLORS } from '../../colors'; 4 | import { Box } from '@mui/system'; 5 | import { getFromDate } from '../utils'; 6 | 7 | type GraphLabelProps = { 8 | label: string; 9 | value: string; 10 | selectedValue: string; 11 | setSelectedValue: (value: string) => void; 12 | textColor?: string; 13 | color?: string; 14 | }; 15 | 16 | const GraphLabel = ({ 17 | label, 18 | value, 19 | selectedValue, 20 | setSelectedValue, 21 | textColor, 22 | color, 23 | }: GraphLabelProps) => { 24 | return ( 25 | setSelectedValue(value)} 28 | sx={{ 29 | backgroundColor: color, 30 | color: textColor, 31 | outline: selectedValue === value ? 1 : 0, 32 | outlineColor: BASIC_COLORS.BLACK, 33 | }} 34 | /> 35 | ); 36 | }; 37 | 38 | type GraphLabelsProps = { 39 | graphTimeline: string; 40 | setGraphTimeline: (value: string) => void; 41 | setFromDate: (value: Date) => void; 42 | }; 43 | 44 | export const GraphLabels = ({ 45 | graphTimeline, 46 | setGraphTimeline, 47 | setFromDate, 48 | }: GraphLabelsProps) => { 49 | const todayDate = useMemo(() => new Date(), []); 50 | const handleTimelineChange = (value: string) => { 51 | setGraphTimeline(value); 52 | setFromDate(getFromDate(value, todayDate)); 53 | }; 54 | 55 | return ( 56 | 57 | 63 | 69 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/EditOrGenerateCatalogInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; 2 | import { Box, Card } from '@backstage/ui'; 3 | import Link from '@material-ui/core/Link'; 4 | import Divider from '@mui/material/Divider'; 5 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 6 | import { catalogCreatorTranslationRef } from '../../utils/translations'; 7 | import style from '../../catalog.module.css'; 8 | 9 | interface EditOrGenerateCatalogInfoBoxProps { 10 | docsLink?: string; 11 | } 12 | 13 | export const EditOrGenerateCatalogInfoBox = ({ 14 | docsLink, 15 | }: EditOrGenerateCatalogInfoBoxProps) => { 16 | const { t } = useTranslationRef(catalogCreatorTranslationRef); 17 | return ( 18 | 19 | 20 |

{t('infoBox.title')}

21 | 22 |

{t('infoBox.p1')}

23 |

{t('infoBox.p2')}

24 | 25 |

{t('infoBox.subtitle')}

26 |

{t('infoBox.p3')}

27 |

{t('infoBox.systemTitle')}

{' '} 28 |

{t('infoBox.systemParagraph')}

29 |

{t('infoBox.componentTitle')}

{' '} 30 |

{t('infoBox.componentParagraph')}

31 |

{t('infoBox.APITitle')}

32 |

{t('infoBox.APIParagraph')}

33 |

34 | {t('infoBox.APIremark')} 35 |

36 |

{t('infoBox.resourceTitle')}

37 |

{t('infoBox.resourceParagraph')}

38 | {docsLink && ( 39 |
40 | {t('infoBox.linkText')} 41 |
42 | )} 43 |
44 | 49 | {t('infoBox.linkText2')} 50 | 51 | 52 |
53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityDistribution.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/system'; 2 | import { SEVERITY_COLORS } from '../colors'; 3 | import { getTotalVulnerabilityCount } from '../mapping/getSeverityCounts'; 4 | import { SeverityCount } from '../typesFrontend'; 5 | import Tooltip from '@mui/material/Tooltip'; 6 | 7 | type Props = { 8 | severityCount: SeverityCount; 9 | highestVulnerabilityCount: number; 10 | }; 11 | 12 | export const VulnerabilityDistribution = ({ 13 | severityCount, 14 | highestVulnerabilityCount, 15 | }: Props) => { 16 | const { critical, high, medium, low, negligible, unknown } = severityCount; 17 | const totalRowVulnerabilities = getTotalVulnerabilityCount(severityCount); 18 | 19 | const severityLevels = [ 20 | { label: 'Critical', color: SEVERITY_COLORS.CRITICAL, count: critical }, 21 | { label: 'High', color: SEVERITY_COLORS.HIGH, count: high }, 22 | { label: 'Medium', color: SEVERITY_COLORS.MEDIUM, count: medium }, 23 | { label: 'Low', color: SEVERITY_COLORS.LOW, count: low }, 24 | { 25 | label: 'Negligible', 26 | color: SEVERITY_COLORS.NEGLIGIBLE, 27 | count: negligible, 28 | }, 29 | { label: 'Unknown', color: SEVERITY_COLORS.UNKNOWN, count: unknown }, 30 | ]; 31 | 32 | const globalFraction = 33 | (totalRowVulnerabilities / highestVulnerabilityCount) * 100; 34 | const getFraction = (count: number) => 35 | (count / totalRowVulnerabilities) * 100; 36 | 37 | return ( 38 | 45 | {severityLevels.map(({ color, count, label }) => { 46 | const fraction = getFraction(count); 47 | return ( 48 | 49 | 57 | 58 | ); 59 | })} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/logo/kv-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 47 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/hooks/useFetchEntities.ts: -------------------------------------------------------------------------------- 1 | import { Control, useWatch } from 'react-hook-form'; 2 | import { formSchema } from '../schemas/formSchema'; 3 | import z from 'zod/v4'; 4 | import { useAsync } from 'react-use'; 5 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 6 | import { useApi } from '@backstage/core-plugin-api'; 7 | import { Entity } from '@backstage/catalog-model'; 8 | import { useMemo } from 'react'; 9 | 10 | export const useFetchEntities = ( 11 | control: Control>, 12 | entityKind: string, 13 | ) => { 14 | const catalogApi = useApi(catalogApiRef); 15 | 16 | const fetchEntities = useAsync(async () => { 17 | const results = await catalogApi.getEntities({ 18 | filter: { kind: entityKind }, 19 | }); 20 | 21 | return results.items as Entity[]; 22 | }, [catalogApi]); 23 | 24 | const formEntities = useWatch({ 25 | control, 26 | name: 'entities', 27 | }); 28 | 29 | const combined: Entity[] = useMemo(() => { 30 | const fromFormEntities = [ 31 | ...formEntities.flatMap(e => { 32 | if (e.kind === entityKind && e.name !== '') { 33 | return { 34 | apiVersion: 'backstage.io/v1alpha1', 35 | kind: e.kind, 36 | metadata: { 37 | name: e.name, 38 | }, 39 | spec: { 40 | title: e.title, 41 | }, 42 | }; 43 | } 44 | return []; 45 | }), 46 | ]; 47 | const fetchedEntities = [ 48 | ...(fetchEntities.value 49 | ? fetchEntities.value.filter(e => { 50 | if ( 51 | fromFormEntities 52 | .map(entity => `${entity.kind}:default/${entity.metadata.name}`) 53 | .includes(`${e.kind}:default/${e.metadata.name}`) 54 | ) { 55 | return false; 56 | } 57 | return true; 58 | }) 59 | : []), 60 | ]; 61 | return [...fromFormEntities, ...fetchedEntities]; 62 | }, [fetchEntities.value, formEntities, entityKind]); 63 | 64 | return { 65 | loading: fetchEntities.loading, 66 | error: fetchEntities.error, 67 | value: combined, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/SecretsOverview/Secret.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import { useTheme } from '@mui/material/styles'; 3 | import Typography from '@mui/material/Typography'; 4 | import { Stack } from '@mui/system'; 5 | import { formatDate } from 'date-fns'; 6 | import { Link } from '@backstage/core-components'; 7 | import { SecretAlert } from '../../typesFrontend'; 8 | import Chip from '@mui/material/Chip'; 9 | import ListItem from '@mui/material/ListItem'; 10 | 11 | type Props = { 12 | secret: SecretAlert; 13 | }; 14 | 15 | export const Secret = ({ secret }: Props) => { 16 | const theme = useTheme(); 17 | const isDarkMode = theme.palette.mode === 'dark'; 18 | 19 | return ( 20 | 26 | 27 | 32 | {secret.summary} 33 | {secret.bypassed && ( 34 | 40 | )} 41 | 42 | 43 | 44 | {`Hemmelighet: ${secret.secretValue}`} 45 | 46 | Oppdaget: {formatDate(secret.createdAt, 'dd.MM.yyyy HH:mm')} 47 | 48 | {secret.htmlUrl && ( 49 | 50 | GitHub-link:{' '} 51 | 52 | {new URL(secret.htmlUrl).href} 53 | 54 | 55 | )} 56 | {secret.bypassedBy && ( 57 | 58 | {`Omgått av: ${secret.bypassedBy.name}`}{' '} 59 | {secret.bypassedBy.isRepositoryAdmin && '(Admin)'} 60 | 61 | )} 62 | 63 | 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/utils/getRepoInfo.ts: -------------------------------------------------------------------------------- 1 | import { OAuthApi } from '@backstage/core-plugin-api'; 2 | import { Octokit } from '@octokit/rest'; 3 | 4 | export async function getRepoInfo( 5 | url: string, 6 | githubAuthApi: OAuthApi, 7 | canNotFindRepoErrorMsg: string, 8 | ) { 9 | const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/); 10 | 11 | if (!match) { 12 | throw new Error('Invalid GitHub repository URL'); 13 | } 14 | const owner = match[1]; 15 | const repo = match[2]; 16 | 17 | const returnObject: { 18 | default_branch?: string; 19 | existingPrUrl?: string; 20 | } = {}; 21 | 22 | const octokit = new Octokit({ 23 | auth: await githubAuthApi.getAccessToken(), 24 | }); 25 | 26 | try { 27 | const response = await octokit.rest.repos.get({ 28 | owner: owner, 29 | repo: repo, 30 | headers: { 31 | 'If-None-Match': '', 32 | }, 33 | }); 34 | 35 | returnObject.default_branch = response.data.default_branch; 36 | } catch (error: unknown) { 37 | if (error instanceof Error) { 38 | error.message = `${canNotFindRepoErrorMsg}${url}`; 39 | throw error; 40 | } else { 41 | throw new Error( 42 | 'Unkown error when trying to find the GitHub repository.', 43 | ); 44 | } 45 | } 46 | 47 | try { 48 | const response = await octokit.rest.pulls.list({ 49 | owner: owner, 50 | repo: repo, 51 | state: 'open', 52 | headers: { 53 | 'If-None-Match': '', 54 | }, 55 | }); 56 | 57 | const matchingPr = response.data.find( 58 | pr => pr.title === 'Create/update catalog-info.yaml', 59 | ); 60 | 61 | if (matchingPr) { 62 | returnObject.existingPrUrl = matchingPr.html_url; 63 | } 64 | } catch (error: unknown) { 65 | if (error instanceof Error) { 66 | if (error.message.toLowerCase().includes('credentials')) { 67 | throw error; 68 | } 69 | error.message = `Could not check if the pull request already exists for: ${url}.`; 70 | throw error; 71 | } else { 72 | throw new Error( 73 | 'Unkown error when trying to find the GitHub repository.', 74 | ); 75 | } 76 | } 77 | return returnObject; 78 | } 79 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/catalog.module.css: -------------------------------------------------------------------------------- 1 | .loadingContainer { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 1.5rem; 6 | min-height: 10rem; 7 | } 8 | 9 | .repositoryCard { 10 | position: relative; 11 | overflow: visible; 12 | } 13 | 14 | .loadingOverlay { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | z-index: 1; 24 | } 25 | 26 | .darkLoadingOverlay { 27 | background-color: rgba(118, 118, 118, 0.4); 28 | } 29 | 30 | .lightLoadingOverlay { 31 | background-color: rgba(255, 255, 255, 0.7); 32 | } 33 | 34 | .learnMoreLink { 35 | margin: 1.5rem 0; 36 | } 37 | 38 | .alert { 39 | font-weight: bold; 40 | text-align: center; 41 | } 42 | 43 | .errorText { 44 | color: red; 45 | font-size: 0.75rem; 46 | } 47 | 48 | .hidden { 49 | visibility: hidden; 50 | } 51 | 52 | .fieldHeaderContainer { 53 | justify-self: start; 54 | align-items: center; 55 | } 56 | 57 | .label { 58 | font-size: 0.75rem; 59 | margin: 0; 60 | display: inline-flex; 61 | align-items: center; 62 | } 63 | 64 | .fieldHeaderTooltip { 65 | cursor: help; 66 | color: #cbcbcbff; 67 | display: flex; 68 | margin-bottom: 0.1rem; 69 | } 70 | 71 | .requiredMark { 72 | color: #ff0000; 73 | font-size: 1rem; 74 | } 75 | 76 | .textField { 77 | font-size: 0.85rem; 78 | font-family: system-ui!; 79 | } 80 | 81 | .entityCard { 82 | margin-right: 1rem; 83 | margin-bottom: 1rem; 84 | padding: 1rem; 85 | position: relative; 86 | overflow: visible; 87 | display: flex; 88 | flex-direction: column; 89 | justify-items: start; 90 | gap: var(--bui-space-4); 91 | } 92 | 93 | .addEntityTitle { 94 | font-weight: bold; 95 | margin-top: 1.5rem; 96 | } 97 | 98 | .endOfFormDivider { 99 | margin: 1.5rem 0; 100 | } 101 | 102 | .createPRButton { 103 | margin-bottom: 0.75rem; 104 | } 105 | 106 | .formInfoText { 107 | font-size: 0.85rem; 108 | color: gray; 109 | } 110 | 111 | .deleteEntityButton { 112 | width: 40px; 113 | align-self: flex-end; 114 | } 115 | -------------------------------------------------------------------------------- /plugins/catalog-creator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kartverket/backstage-plugin-catalog-creator", 3 | "version": "1.1.5", 4 | "license": "Apache-2.0", 5 | "main": "src/index.ts", 6 | "types": "src/index.ts", 7 | "publishConfig": { 8 | "access": "public", 9 | "main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts" 11 | }, 12 | "backstage": { 13 | "role": "frontend-plugin", 14 | "pluginId": "catalog-creator", 15 | "pluginPackages": [ 16 | "@kartverket/backstage-plugin-catalog-creator" 17 | ] 18 | }, 19 | "sideEffects": false, 20 | "scripts": { 21 | "start": "backstage-cli package start", 22 | "build": "backstage-cli package build", 23 | "lint": "backstage-cli package lint", 24 | "clean": "backstage-cli package clean", 25 | "prepack": "backstage-cli package prepack", 26 | "postpack": "backstage-cli package postpack", 27 | "tsc": "tsc", 28 | "publishToNpm": "npm publish" 29 | }, 30 | "dependencies": { 31 | "@backstage/catalog-model": "backstage:^", 32 | "@backstage/core-components": "backstage:^", 33 | "@backstage/core-plugin-api": "backstage:^", 34 | "@backstage/plugin-catalog-import": "backstage:^", 35 | "@backstage/plugin-catalog-react": "backstage:^", 36 | "@backstage/theme": "backstage:^", 37 | "@backstage/ui": "backstage:^", 38 | "@hookform/resolvers": "^5.2.2", 39 | "@mui/icons-material": "^7.3.4", 40 | "@octokit/core": "^7.0.5", 41 | "@octokit/rest": "^22.0.0", 42 | "octokit-plugin-create-pull-request": "^6.0.1", 43 | "react-hook-form": "^7.63.0", 44 | "react-use": "^17.2.4", 45 | "yaml": "^2.8.1" 46 | }, 47 | "peerDependencies": { 48 | "@material-ui/core": "^4.9.13", 49 | "@material-ui/icons": "^4.9.1", 50 | "@material-ui/lab": "^4.0.0-alpha.61", 51 | "@mui/material": "^5.16.4", 52 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0", 53 | "zod": "^3.25.0 || ^4.0.0" 54 | }, 55 | "devDependencies": { 56 | "@backstage/cli": "backstage:^", 57 | "@backstage/core-app-api": "backstage:^", 58 | "@backstage/dev-utils": "backstage:^", 59 | "msw": "^1.0.0", 60 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0" 61 | }, 62 | "files": [ 63 | "dist" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/ScannerStatus/ComponentScannerStatus.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleOutlined, HighlightOffOutlined } from '@mui/icons-material'; 2 | import Typography from '@mui/material/Typography'; 3 | 4 | import type { RepositoryScannerStatusData } from '../../typesFrontend'; 5 | import { CardTitle } from '../CardTitle'; 6 | import { StyledTableRow } from '../TableRow'; 7 | import Stack from '@mui/material/Stack'; 8 | import Table from '@mui/material/Table'; 9 | import TableBody from '@mui/material/TableBody'; 10 | import TableCell from '@mui/material/TableCell'; 11 | import Tooltip from '@mui/material/Tooltip'; 12 | import { ScannerInfo } from './ScannerInfo'; 13 | 14 | type ComponentScannerStatusProps = { 15 | scannerStatus: RepositoryScannerStatusData; 16 | }; 17 | 18 | export const ComponentScannerStatus = ({ 19 | scannerStatus, 20 | }: ComponentScannerStatusProps) => { 21 | if (!scannerStatus) { 22 | return ( 23 | 24 | 25 | 26 | Vi fant dessverre ingen status på skannere. 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | {scannerStatus.scannerStatus.map(status => ( 39 | 40 | 41 | 42 | 43 | 44 | {status.on ? ( 45 | 46 | 47 | 48 | ) : ( 49 | 50 | 51 | 52 | )} 53 | 54 | 55 | ))} 56 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/VulnerabilityTable/TableRowCollapse.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Box } from '@mui/system'; 2 | import { formatDate } from 'date-fns'; 3 | 4 | import { SCANNER_CARD } from '../../colors'; 5 | import { Vulnerability } from '../../typesFrontend'; 6 | import { ScannerDetails } from './ScannerDetails/ScannerDetails'; 7 | import Collapse from '@mui/material/Collapse'; 8 | import Typography from '@mui/material/Typography'; 9 | import { useTheme } from '@mui/system'; 10 | 11 | interface Props { 12 | vulnerability: Vulnerability; 13 | open: boolean; 14 | } 15 | 16 | export const TableRowCollapse = ({ vulnerability, open }: Props) => { 17 | const theme = useTheme(); 18 | 19 | return ( 20 | 21 | 22 | 23 | {vulnerability.scanners.map(s => ( 24 | 25 | ))} 26 | 27 | {vulnerability.acceptedAt && ( 28 | 44 | 45 | Akseptert av: 46 | {vulnerability.acceptedBy} 47 | 48 | 49 | 50 | Dato: 51 | {formatDate(vulnerability.acceptedAt, 'dd.MM.yyyy')} 52 | 53 | {vulnerability.comment && ( 54 | 55 | Kommentar: 56 | {vulnerability.comment} 57 | 58 | )} 59 | 60 | )} 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogForm/Forms/DomainForm.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from '@backstage/ui'; 2 | import { Control } from 'react-hook-form'; 3 | import { DomainTypes, EntityErrors } from '../../../types/types'; 4 | import { formSchema } from '../../../schemas/formSchema'; 5 | import z from 'zod/v4'; 6 | 7 | import { Entity } from '@backstage/catalog-model'; 8 | import { TagField } from '../Autocompletes/TagField'; 9 | import { SingleEntityAutocomplete } from '../Autocompletes/SingleEntityAutocomplete'; 10 | import { SingleSelectAutocomplete } from '../Autocompletes/SingleSelectAutocomplete'; 11 | import { MultipleEntitiesAutocomplete } from '../Autocompletes/MultipleEntitiesAutocomplete'; 12 | 13 | export type DomainFormProps = { 14 | index: number; 15 | control: Control>; 16 | errors: EntityErrors<'Domain'>; 17 | groups: Entity[]; 18 | domains: Entity[]; 19 | }; 20 | 21 | export const DomainForm = ({ 22 | index, 23 | control, 24 | errors, 25 | groups, 26 | domains, 27 | }: DomainFormProps) => { 28 | return ( 29 | 30 |
31 | 39 |
40 | 41 |
42 | 51 |
52 |
53 |
54 | 63 |
64 | 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /plugins/security-metrics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kartverket/backstage-plugin-security-metrics-frontend", 3 | "version": "3.18.1", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "publishConfig": { 8 | "access": "public", 9 | "main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts" 11 | }, 12 | "backstage": { 13 | "role": "frontend-plugin", 14 | "pluginId": "security-metrics-frontend", 15 | "pluginPackages": [ 16 | "@kartverket/backstage-plugin-security-metrics-frontend" 17 | ] 18 | }, 19 | "sideEffects": false, 20 | "scripts": { 21 | "start": "backstage-cli package start", 22 | "build": "backstage-cli package build", 23 | "lint": "backstage-cli package lint", 24 | "clean": "backstage-cli package clean", 25 | "prepack": "backstage-cli package prepack", 26 | "postpack": "backstage-cli package postpack", 27 | "publishToNpm": "npm publish", 28 | "tsc": "tsc" 29 | }, 30 | "dependencies": { 31 | "@backstage/catalog-client": "backstage:^", 32 | "@backstage/catalog-model": "backstage:^", 33 | "@backstage/config": "backstage:^", 34 | "@backstage/core-components": "backstage:^", 35 | "@backstage/core-plugin-api": "backstage:^", 36 | "@backstage/plugin-catalog-react": "backstage:^", 37 | "@backstage/theme": "backstage:^", 38 | "@emotion/cache": "^11.13.1", 39 | "@emotion/react": "^11.13.3", 40 | "@emotion/styled": "^11.13.0", 41 | "@mui/icons-material": "^6.1.5", 42 | "@mui/material": "^6.1.5", 43 | "@mui/system": "^6.1.5", 44 | "@mui/x-charts": "^7.21.0", 45 | "@mui/x-date-pickers": "^7.21.0", 46 | "@tanstack/react-query": "^5.84.1", 47 | "@tanstack/react-query-devtools": "^5.84.1", 48 | "date-fns": "^4.1.0", 49 | "react-use": "^17.5.1", 50 | "recharts": "^2.13.0" 51 | }, 52 | "peerDependencies": { 53 | "react": "^18.0.0", 54 | "react-dom": "^18.0.0", 55 | "react-router-dom": "^6.26.2" 56 | }, 57 | "devDependencies": { 58 | "@backstage/cli": "backstage:^", 59 | "@backstage/core-app-api": "backstage:^", 60 | "@backstage/dev-utils": "backstage:^", 61 | "@tanstack/eslint-plugin-query": "^5.83.1", 62 | "msw": "^2.5.1" 63 | }, 64 | "files": [ 65 | "dist" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/SecretsOverview/SecretsDialog.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from '@mui/icons-material/Close'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogContent from '@mui/material/DialogContent'; 4 | import DialogTitle from '@mui/material/DialogTitle'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Typography from '@mui/material/Typography'; 7 | import { Stack } from '@mui/system'; 8 | 9 | import { SecretAlert } from '../../typesFrontend'; 10 | import { Secret } from './Secret'; 11 | import List from '@mui/material/List'; 12 | 13 | export interface Secrets { 14 | componentName: string; 15 | alerts: SecretAlert[]; 16 | } 17 | 18 | export interface SecretProps { 19 | secretsOverviewData: Secrets[]; 20 | openDialog: boolean; 21 | closeDialogBox: () => void; 22 | } 23 | 24 | export const SecretsDialog = ({ 25 | secretsOverviewData, 26 | openDialog, 27 | closeDialogBox, 28 | }: SecretProps) => { 29 | return ( 30 | 38 | 39 | Eksponerte hemmeligheter 40 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {secretsOverviewData.map(repo => ( 55 |
56 | 57 | {repo.componentName}: 58 | 59 | 60 | {repo.alerts && 61 | repo.alerts.map((secret, index) => ( 62 | 63 | ))} 64 | 65 |
66 | ))} 67 |
68 |
69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /plugins/security-champion/src/hooks/useChangeSecurityChampionsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import { SecurityChamp } from '../types'; 4 | import { post } from '../api/client'; 5 | import { getBackstageToken } from '../utils/authenticationUtils'; 6 | import { 7 | configApiRef, 8 | identityApiRef, 9 | useApi, 10 | } from '@backstage/core-plugin-api'; 11 | 12 | export const useSetSecurityChampionMutation = () => { 13 | const backendUrl = useApi(configApiRef).getString('backend.baseUrl'); 14 | const backstageAuthApi = useApi(identityApiRef); 15 | const queryClient = useQueryClient(); 16 | 17 | return useMutation({ 18 | mutationFn: async (securityChampion: SecurityChamp) => { 19 | const { backstageToken } = await getBackstageToken(backstageAuthApi); 20 | const { email } = await backstageAuthApi.getProfileInfo(); 21 | 22 | const userEmail = email ? email : 'no user email'; 23 | 24 | const endpointUrl = `${backendUrl}/api/proxy/security-champion-proxy/api/setSecurityChampion`; 25 | 26 | return post< 27 | { 28 | repositoryName: string; 29 | securityChampionEmail: string; 30 | modifiedBy: string; 31 | }, 32 | string 33 | >(endpointUrl, backstageToken, { 34 | repositoryName: securityChampion.repositoryName, 35 | securityChampionEmail: securityChampion.securityChampionEmail, 36 | modifiedBy: userEmail, 37 | }); 38 | }, 39 | onMutate: async (newChampion: SecurityChamp) => { 40 | const queryKey = ['security-champions', [newChampion.repositoryName]]; 41 | 42 | await queryClient.cancelQueries({ queryKey }); 43 | 44 | const previousChampions = 45 | queryClient.getQueryData(queryKey); 46 | 47 | queryClient.setQueryData(queryKey, old => { 48 | if (!old) return [newChampion]; 49 | 50 | const existingIndex = old.findIndex( 51 | champ => champ.repositoryName === newChampion.repositoryName, 52 | ); 53 | 54 | if (existingIndex >= 0) { 55 | const updated = [...old]; 56 | updated[existingIndex] = newChampion; 57 | return updated; 58 | } 59 | return [...old, newChampion]; 60 | }); 61 | 62 | return { previousChampions, queryKey }; 63 | }, 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # example-backend 2 | 3 | This package is an EXAMPLE of a Backstage backend. 4 | 5 | The main purpose of this package is to provide a test bed for Backstage plugins 6 | that have a backend part. Feel free to experiment locally or within your fork by 7 | adding dependencies and routes to this backend, to try things out. 8 | 9 | Our goal is to eventually amend the create-app flow of the CLI, such that a 10 | production ready version of a backend skeleton is made alongside the frontend 11 | app. Until then, feel free to experiment here! 12 | 13 | ## Development 14 | 15 | To run the example backend, first go to the project root and run 16 | 17 | ```bash 18 | yarn install 19 | ``` 20 | 21 | You should only need to do this once. 22 | 23 | After that, go to the `packages/backend` directory and run 24 | 25 | ```bash 26 | yarn start 27 | ``` 28 | 29 | If you want to override any configuration locally, for example adding any secrets, 30 | you can do so in `app-config.local.yaml`. 31 | 32 | The backend starts up on port 7007 per default. 33 | 34 | ## Populating The Catalog 35 | 36 | If you want to use the catalog functionality, you need to add so called 37 | locations to the backend. These are places where the backend can find some 38 | entity descriptor data to consume and serve. For more information, see 39 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog). 40 | 41 | To get started quickly, this template already includes some statically configured example locations 42 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 43 | like, and also override them for local development in `app-config.local.yaml`. 44 | 45 | ## Authentication 46 | 47 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 48 | to its comprehensive set of supported authentication 49 | [strategies](http://www.passportjs.org/packages/). 50 | 51 | Read more about the 52 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 53 | and 54 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 55 | 56 | ## Documentation 57 | 58 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 59 | - [Backstage Documentation](https://backstage.io/docs) 60 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogForm/Forms/SystemForm.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from '@backstage/ui'; 2 | import { Control, UseFormSetValue, useWatch } from 'react-hook-form'; 3 | import { EntityErrors, SystemTypes } from '../../../types/types'; 4 | import { formSchema } from '../../../schemas/formSchema'; 5 | import z from 'zod/v4'; 6 | import { Entity } from '@backstage/catalog-model'; 7 | import { useUpdateDependentFormFields } from '../../../hooks/useUpdateDependentFormFields'; 8 | import { TagField } from '../Autocompletes/TagField'; 9 | import { SingleEntityAutocomplete } from '../Autocompletes/SingleEntityAutocomplete'; 10 | import { SingleSelectAutocomplete } from '../Autocompletes/SingleSelectAutocomplete'; 11 | 12 | export type SystemFormProps = { 13 | index: number; 14 | control: Control>; 15 | setValue: UseFormSetValue>; 16 | errors: EntityErrors<'System'>; 17 | groups: Entity[]; 18 | domains: Entity[]; 19 | }; 20 | 21 | export const SystemForm = ({ 22 | index, 23 | control, 24 | setValue, 25 | errors, 26 | groups, 27 | domains, 28 | }: SystemFormProps) => { 29 | const domainVal = useWatch({ 30 | control, 31 | name: `entities.${index}.domain`, 32 | }); 33 | 34 | useUpdateDependentFormFields( 35 | domains, 36 | domainVal ? [domainVal] : undefined, 37 | `entities.${index}.domain`, 38 | setValue, 39 | ); 40 | 41 | return ( 42 | 43 |
44 | 51 |
52 | 53 |
54 | 63 |
64 |
65 |
66 | 74 |
75 | 76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogCreatorPage/StatusMessages.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; 2 | import Alert from '@mui/material/Alert'; 3 | import Link from '@mui/material/Link'; 4 | import { catalogCreatorTranslationRef } from '../../utils/translations'; 5 | 6 | interface StatusMessagesProps { 7 | hasExistingCatalogFile: boolean; 8 | shouldCreateNewFile: boolean; 9 | hasError: boolean; 10 | isLoading: boolean; 11 | repoStateError: boolean; 12 | showForm: boolean; 13 | existingPrUrl?: string; 14 | analysisError?: Error; 15 | repoStateErrorMessage?: string; 16 | repoInfoError?: Error; 17 | catalogInfoError?: Error; 18 | } 19 | 20 | export const StatusMessages = ({ 21 | hasExistingCatalogFile, 22 | shouldCreateNewFile, 23 | hasError, 24 | isLoading, 25 | repoStateError, 26 | showForm, 27 | existingPrUrl, 28 | analysisError, 29 | repoStateErrorMessage, 30 | repoInfoError, 31 | catalogInfoError, 32 | }: StatusMessagesProps) => { 33 | const { t } = useTranslationRef(catalogCreatorTranslationRef); 34 | return ( 35 | <> 36 | {hasExistingCatalogFile && 37 | !(hasError || isLoading || repoStateError) && 38 | showForm && ( 39 | 40 | {t('form.infoAlerts.alreadyExists')} 41 | 42 | )} 43 | 44 | {shouldCreateNewFile && !(hasError || isLoading || repoStateError) && ( 45 | 46 | {t('form.infoAlerts.doesNotExist')} 47 | 48 | )} 49 | 50 | {existingPrUrl && !isLoading && ( 51 | 52 | {t('form.knownErrorAlerts.PRExists')}:{' '} 53 | 59 | {existingPrUrl} 60 | 61 | 62 | )} 63 | 64 | {analysisError && ( 65 | 66 | {analysisError.message} 67 | 68 | )} 69 | 70 | {repoStateError && ( 71 | 72 | {repoStateErrorMessage} 73 | 74 | )} 75 | 76 | {repoInfoError && ( 77 | 78 | {repoInfoError.message} 79 | 80 | )} 81 | 82 | {catalogInfoError && ( 83 | 84 | {catalogInfoError.message} 85 | 86 | )} 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /plugins/catalog-creator/src/components/CatalogForm/Autocompletes/TagField.tsx: -------------------------------------------------------------------------------- 1 | import { Control, Controller } from 'react-hook-form'; 2 | import { FieldHeader } from '../FieldHeader'; 3 | import Autocomplete from '@mui/material/Autocomplete'; 4 | import TextField from '@mui/material/TextField'; 5 | import { formSchema } from '../../../schemas/formSchema'; 6 | import z from 'zod/v4'; 7 | import { EntityErrors, Kind } from '../../../types/types'; 8 | import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; 9 | import { catalogCreatorTranslationRef } from '../../../utils/translations'; 10 | import { useState } from 'react'; 11 | 12 | import style from '../../../catalog.module.css'; 13 | 14 | type TagFieldProps = { 15 | index: number; 16 | control: Control>; 17 | errors: EntityErrors; 18 | options: string[]; 19 | }; 20 | 21 | export const TagField = ({ 22 | index, 23 | control, 24 | errors, 25 | options, 26 | }: TagFieldProps) => { 27 | const { t } = useTranslationRef(catalogCreatorTranslationRef); 28 | const [inputValue, setInputValue] = useState(''); 29 | return ( 30 |
31 | 35 | ( 39 |
40 | field.onChange(value)} 43 | inputValue={inputValue} 44 | onInputChange={(_, newInputValue) => { 45 | setInputValue(newInputValue); 46 | }} 47 | value={field.value || []} 48 | multiple 49 | freeSolo 50 | options={ 51 | inputValue && !options.includes(inputValue) 52 | ? [...options, inputValue] 53 | : options 54 | } 55 | size="small" 56 | renderInput={params => ( 57 | 65 | )} 66 | /> 67 |
68 | )} 69 | /> 70 | 71 | 74 | {errors?.tags?.message 75 | ? t(errors?.tags?.message as keyof typeof t) 76 | : '\u00A0'} 77 | 78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createBackend } from '@backstage/backend-defaults'; 2 | import { authModuleMicrosoftProvider } from './plugins/extensions/auth'; 3 | import { msGroupTransformerCatalogModule } from './plugins/extensions/catalog'; 4 | import { catalogNotificationsModule } from './plugins/extensions/catalogNotificationsModule'; 5 | 6 | const backend = createBackend(); 7 | 8 | // App 9 | backend.add(import('@backstage/plugin-app-backend')); 10 | 11 | // Auth 12 | backend.add(import('@backstage/plugin-auth-backend')); 13 | backend.add(authModuleMicrosoftProvider); 14 | backend.add(import('@backstage/plugin-auth-backend-module-google-provider')); // Required for ROS Plugin 15 | backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); // Required for ROS Plugin 16 | backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); 17 | 18 | // Catalog 19 | backend.add(import('@backstage/plugin-catalog-backend')); 20 | backend.add(import('@backstage/plugin-catalog-backend-module-github')); 21 | backend.add(import('@backstage/plugin-catalog-backend-module-msgraph')); 22 | backend.add(msGroupTransformerCatalogModule); 23 | backend.add( 24 | import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), 25 | ); 26 | backend.add(import('@backstage/plugin-catalog-backend-module-logs')); 27 | backend.add(import('@backstage/plugin-catalog-backend-module-openapi')); 28 | 29 | // Explore 30 | backend.add(import('@backstage-community/plugin-explore-backend')); 31 | 32 | // DASK 33 | backend.add(import('@kartverket/plugin-dask-onboarding-backend')); 34 | 35 | // Devtools 36 | backend.add(import('@backstage/plugin-devtools-backend')); 37 | 38 | // Lighthouse 39 | backend.add(import('@backstage-community/plugin-lighthouse-backend')); 40 | 41 | // Proxy 42 | backend.add(import('@backstage/plugin-proxy-backend')); 43 | 44 | // Scaffolder 45 | backend.add(import('@backstage/plugin-scaffolder-backend')); 46 | backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); 47 | 48 | // Search 49 | backend.add(import('@backstage/plugin-search-backend')); 50 | backend.add(import('@backstage/plugin-search-backend-module-catalog')); 51 | backend.add(import('@backstage/plugin-search-backend-module-techdocs')); 52 | backend.add(import('@backstage/plugin-search-backend-module-pg')); 53 | 54 | // TechDocs 55 | backend.add(import('@backstage/plugin-techdocs-backend')); 56 | 57 | // Security metrics 58 | backend.add(import('@kartverket/backstage-plugin-security-metrics-backend')); 59 | 60 | // Notifications 61 | backend.add(import('@backstage/plugin-notifications-backend')); 62 | backend.add(catalogNotificationsModule); 63 | backend.add(import('@backstage/plugin-signals-backend')); 64 | backend.add(import('@internal/plugin-catalog-backend-module-function-kind')); 65 | 66 | backend.start(); 67 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/Trend/TrendGraph.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | import { useMemo } from 'react'; 3 | import { 4 | ResponsiveContainer, 5 | AreaChart, 6 | XAxis, 7 | YAxis, 8 | Tooltip, 9 | Area, 10 | } from 'recharts'; 11 | import { BASIC_COLORS, SEVERITY_COLORS } from '../../colors'; 12 | import { yAxisAdjustment } from '../utils'; 13 | import { LinearGradient } from './LinearGradient'; 14 | import { TrendSeverityCounts } from '../../typesFrontend'; 15 | import { getAggregatedTrends } from './utils'; 16 | import { useTheme } from '@mui/material/styles'; 17 | 18 | interface GraphProps { 19 | trendData: TrendSeverityCounts[]; 20 | graphTimeline: string; 21 | } 22 | 23 | export const Graph = ({ trendData, graphTimeline }: GraphProps) => { 24 | const theme = useTheme(); 25 | const isDarkMode = theme.palette.mode === 'dark'; 26 | 27 | const data = useMemo(() => { 28 | const aggregatedTrends = getAggregatedTrends(trendData); 29 | return aggregatedTrends.sort( 30 | (a, b) => 31 | new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), 32 | ); 33 | }, [trendData]); 34 | 35 | return ( 36 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | graphTimeline === 'oneYear' 50 | ? format(new Date(timestamp), 'dd-MM-yyyy') 51 | : format(new Date(timestamp), 'dd-MM') 52 | } 53 | /> 54 | 55 | 63 | format(new Date(timestamp), 'dd-MM-yyyy') 64 | } 65 | /> 66 | 75 | 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_gcp.yml: -------------------------------------------------------------------------------- 1 | name: Push to Google Artifact Registry 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - spire-dev 10 | 11 | env: 12 | PROJECT_ID: spire-kartverket-dev 13 | SERVICE: kartverket-dev-env 14 | REGION: europe-west1 15 | WORKLOAD_IDENTITY_PROVIDER: projects/914366947816/locations/global/workloadIdentityPools/github-pool/providers/github-provider 16 | AR_REPO_LOCATION: europe-west1 17 | AR_URL: europe-west1-docker.pkg.dev/spire-kartverket-dev/backstagespiredev 18 | IMAGE_NAME: backstagespiredev 19 | SERVICE_ACCOUNT: deploy-to-gcp-workflow@spire-kartverket-dev.iam.gserviceaccount.com 20 | 21 | jobs: 22 | push-to-ar: 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | contents: 'read' 27 | id-token: 'write' 28 | 29 | steps: 30 | - uses: 'actions/checkout@v4' 31 | 32 | - uses: 'google-github-actions/auth@v3' 33 | with: 34 | project_id: ${{ env.PROJECT_ID }} 35 | workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} 36 | service_account: ${{env.SERVICE_ACCOUNT}} 37 | 38 | - name: Configure Docker to use the gcloud command-line tool as a credential helper 39 | run: gcloud auth configure-docker ${{ env.AR_REPO_LOCATION }}-docker.pkg.dev --quiet 40 | 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: 22.x # From mise.toml 45 | cache: 'npm' 46 | 47 | - run: corepack enable 48 | 49 | - run: yarn install --immutable 50 | 51 | # tsc outputs type definitions to dist-types/ in the repo root, which are then consumed by the build 52 | - run: yarn tsc 53 | 54 | # Build the backend, which bundles it all up into the packages/backend/dist folder. 55 | # The configuration files here should match the one you use inside the Dockerfile below. 56 | - run: yarn build:backend --config ../../app-config.yaml --config ../../app-config.spiredev.yaml 57 | 58 | - name: Set up Docker Buildx 59 | uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 60 | 61 | - name: Build the Docker image 62 | run: | 63 | docker build -f packages/backend/Dockerfile -t "${{ env.AR_URL }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" . 64 | 65 | - name: Push the Docker image to Google Artifact Registry 66 | run: | 67 | docker push "${{ env.AR_URL }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" 68 | 69 | - name: Deploy to Cloud Run 70 | run: | 71 | gcloud run deploy kartverket-dev-env \ 72 | --image="${{ env.AR_URL }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" \ 73 | --region=europe-west1 \ 74 | --platform=managed \ 75 | -------------------------------------------------------------------------------- /packages/app/public/img/google-cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/components/SecretsOverview/SecretsAlert.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LocalFireDepartment, 3 | CheckCircleOutline, 4 | OpenInFull, 5 | } from '@mui/icons-material'; 6 | import { useMemo, useState } from 'react'; 7 | import { SecretAlert } from '../../typesFrontend'; 8 | import { SecretsDialog } from './SecretsDialog'; 9 | import IconButton from '@mui/material/IconButton'; 10 | import Alert from '@mui/material/Alert'; 11 | import AlertTitle from '@mui/material/AlertTitle'; 12 | 13 | export interface Secrets { 14 | componentName: string; 15 | alerts: SecretAlert[]; 16 | } 17 | 18 | export interface SecretProps { 19 | secretsOverviewData: Secrets[]; 20 | } 21 | 22 | export const SecretsAlert = ({ secretsOverviewData }: SecretProps) => { 23 | const [openDialog, setOpenDialog] = useState(false); 24 | const [onHover, setOnHover] = useState(false); 25 | 26 | const reposWithAlerts = useMemo( 27 | () => secretsOverviewData.filter(r => r.alerts.length > 0), 28 | [secretsOverviewData], 29 | ); 30 | 31 | const totalExposedSecrets = secretsOverviewData.reduce( 32 | (sum, { alerts }) => sum + alerts.length, 33 | 0, 34 | ); 35 | 36 | const hasSecrets = totalExposedSecrets > 0; 37 | 38 | const openDialogBox = () => { 39 | if (hasSecrets) { 40 | setOpenDialog(true); 41 | } 42 | }; 43 | 44 | const closeDialogBox = () => { 45 | setOpenDialog(false); 46 | }; 47 | 48 | const zeroSecretsInfo: string = `Ingen hemmeligheter eksponert`; 49 | const secretsInfo: string = `${totalExposedSecrets} hemmelighet${totalExposedSecrets !== 1 ? 'er' : ''} eksponert. Klikk for å se mer.`; 50 | 51 | return ( 52 | <> 53 | : } 56 | action={ 57 | hasSecrets && ( 58 | } 60 | color="inherit" 61 | onClick={openDialogBox} 62 | /> 63 | ) 64 | } 65 | onClick={openDialogBox} 66 | onMouseEnter={() => { 67 | setOnHover(true); 68 | }} 69 | onMouseLeave={() => { 70 | setOnHover(false); 71 | }} 72 | sx={{ 73 | cursor: onHover && hasSecrets ? 'pointer' : '', 74 | boxShadow: 75 | onHover && hasSecrets 76 | ? '5px 5px 10px 0 rgba(0, 0, 0, 0.1)' 77 | : 'none', 78 | }} 79 | > 80 | {hasSecrets ? secretsInfo : zeroSecretsInfo} 81 | 82 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/api/client.ts: -------------------------------------------------------------------------------- 1 | const ERROR_MESSAGES: Record = { 2 | 401: 'Mangler autentisering. Vennligst logg inn.', 3 | 403: 'Det ser ut som du ikke har tilgang til metrikker for denne ressursen.', 4 | 404: 'Vi fant ikke ressursen du leter etter.', 5 | 500: 'Kunne ikke hente metrikker for denne ressursen på grunn av en server-feil.', 6 | 502: 'Kunne ikke hente metrikker for denne ressursen på grunn av en server-feil.', 7 | 503: 'Kunne ikke hente metrikker for denne ressursen på grunn av en server-feil.', 8 | 504: 'Kunne ikke hente metrikker for denne ressursen på grunn av en server-feil.', 9 | }; 10 | 11 | const DEFAULT_ERROR_MESSAGE = 12 | 'Kunne ikke hente metrikker for denne ressursen på grunn av en ukjent feil.'; 13 | 14 | const throwHttpError = (response: Response): never => { 15 | const message = ERROR_MESSAGES[response.status] ?? DEFAULT_ERROR_MESSAGE; 16 | throw new Error(message); 17 | }; 18 | 19 | const handleResponse = async ( 20 | response: Response, 21 | parse: (response: Response) => Promise, 22 | ): Promise => { 23 | const result = await parse(response); 24 | 25 | if (!response.ok) { 26 | throwHttpError(response); 27 | } 28 | 29 | return result; 30 | }; 31 | 32 | export const post = async ( 33 | url: URL, 34 | backstageToken: string, 35 | requestBody: RequestBody, 36 | ): Promise => { 37 | const response = await fetch(url, { 38 | method: 'POST', 39 | headers: { 40 | 'content-type': 'application/json', 41 | authorization: `Bearer ${backstageToken}`, 42 | }, 43 | body: JSON.stringify(requestBody), 44 | }); 45 | 46 | return handleResponse(response, async res => 47 | res.status === 204 ? (res.text() as unknown as ResponseBody) : res.json(), 48 | ); 49 | }; 50 | 51 | export const put = async ( 52 | url: URL, 53 | backstageToken: string, 54 | requestBody: RequestBody, 55 | ): Promise => { 56 | const response = await fetch(url, { 57 | method: 'PUT', 58 | headers: { 59 | 'content-type': 'application/json', 60 | authorization: `Bearer ${backstageToken}`, 61 | }, 62 | body: JSON.stringify(requestBody), 63 | }); 64 | 65 | return handleResponse(response, async res => 66 | res.status === 204 ? (res.text() as unknown as ResponseBody) : res.json(), 67 | ); 68 | }; 69 | 70 | export const get = async ( 71 | url: URL, 72 | backstageToken: string, 73 | entraIdToken: string, 74 | ): Promise => { 75 | const response = await fetch(url, { 76 | method: 'GET', 77 | headers: { 78 | Authorization: `Bearer ${backstageToken}`, 79 | EntraId: entraIdToken, 80 | }, 81 | }); 82 | 83 | return handleResponse(response, res => res.json()); 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "22" 7 | }, 8 | "scripts": { 9 | "dev": "yarn workspaces foreach -A --include backend --include app --parallel --jobs unlimited -v -i run start", 10 | "start": "yarn workspace app start", 11 | "start-backend": "yarn workspace backend start", 12 | "build:backend": "yarn workspace backend build", 13 | "build:all": "backstage-cli repo build --all", 14 | "build-image": "yarn workspace backend build-image", 15 | "tsc": "tsc", 16 | "tsc:full": "tsc --skipLibCheck false --incremental false", 17 | "clean": "backstage-cli repo clean", 18 | "fix": "backstage-cli repo fix", 19 | "lint": "backstage-cli repo lint --since origin/main", 20 | "lint:all": "backstage-cli repo lint", 21 | "prettier:check": "prettier --check .", 22 | "prettier:format": "prettier --write .", 23 | "new": "backstage-cli new", 24 | "ci//": "echo Installs dependencies without modifying the lockfile.\n`yarn rebuild` was added because folders are randomly missing, this command fixes it.", 25 | "ci": "yarn install --immutable && yarn rebuild", 26 | "iup//": "echo 'Interactive upgrade of dependencies (do not touch packages with `backstage:^?)'", 27 | "iup": "yarn upgrade-interactive", 28 | "cup//": "echo Custom UPdate - Install without lockfile forces yarn to look for latest version within ranges", 29 | "cup": "rm -f yarn.lock && yarn install", 30 | "backstage:upgrade": "backstage-cli versions:bump", 31 | "pipeline//": "echo Shorthand to run all checks in the lint.yml pipeline/workflow", 32 | "pipeline": "yarn run ci && yarn run prettier:check && yarn run lint:all && yarn run tsc", 33 | "self-update//": "Upgrade package manager to the latest version", 34 | "self-update": "corepack up", 35 | "clean-cache": "yarn cache clean --all" 36 | }, 37 | "workspaces": { 38 | "packages": [ 39 | "packages/*", 40 | "plugins/*" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@backstage/cli": "backstage:^", 45 | "@types/node": "^24.9.1", 46 | "lerna": "^7.3.0", 47 | "prettier": "^3.5.3", 48 | "typescript": "~5.8.3" 49 | }, 50 | "resolutions": { 51 | "@types/react": "^18", 52 | "@types/react-dom": "^18", 53 | "@mui/material": "5.16.4", 54 | "jsonpath-plus": "^10.2.0" 55 | }, 56 | "prettier": "@backstage/cli/config/prettier", 57 | "lint-staged": { 58 | "*.{js,jsx,ts,tsx,mjs,cjs}": [ 59 | "eslint --fix", 60 | "prettier --write" 61 | ], 62 | "*.{json,md}": [ 63 | "prettier --write" 64 | ] 65 | }, 66 | "dependencies": { 67 | "@mui/material": "5.16.4", 68 | "@types/react": "^18" 69 | }, 70 | "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538" 71 | } 72 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # This dockerfile builds an image for the backend package. 3 | # It should be executed with the root of the repo as docker context. 4 | # 5 | # Before building this image, be sure to have run the following commands in the repo root: 6 | # 7 | # yarn install 8 | # yarn tsc 9 | # yarn build:backend 10 | # 11 | # Once the commands have been run, you can build the image using `yarn build-image` 12 | 13 | # --- Builder: install prod deps and unpack the bundle 14 | FROM node:22-bookworm-slim AS build 15 | 16 | ENV PYTHON=/usr/bin/python3 17 | RUN corepack enable 18 | 19 | # Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. 20 | # If sqlite3 is not needed anymore, remove libsqlite3-dev and better-sqlite3. 21 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 22 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 23 | apt-get update && \ 24 | apt-get install -y --no-install-recommends python3 g++ build-essential libsqlite3-dev && \ 25 | rm -rf /var/lib/apt/lists/* 26 | 27 | RUN mkdir -p /home/node/.cache 28 | 29 | # This should create the app dir as `node`. 30 | # If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`. 31 | # If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`. 32 | WORKDIR /app 33 | 34 | # Copy files needed by Yarn 35 | COPY .yarn ./.yarn 36 | COPY .yarnrc.yml ./ 37 | COPY backstage.json ./ 38 | 39 | # This switches many Node.js dependencies to production mode. 40 | ENV NODE_ENV=production 41 | 42 | # This disables node snapshot for Node 20 to work with the Scaffolder 43 | # Not sure if needed for Node 22. 44 | ENV NODE_OPTIONS="--no-node-snapshot" 45 | 46 | # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. 47 | # The skeleton contains the package.json of each package in the monorepo, 48 | # and along with yarn.lock and the root package.json, that's enough to run yarn install. 49 | COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ 50 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 51 | 52 | RUN --mount=type=cache,target=/home/node/.cache,sharing=locked,uid=1000,gid=1000 \ 53 | yarn workspaces focus -A --production && yarn cache clean 54 | 55 | # Then copy the rest of the backend bundle, along with any other files we might want. 56 | COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ 57 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 58 | 59 | # --- Runtime: distroless NodeJS 60 | FROM gcr.io/distroless/nodejs22-debian12 61 | ENV NODE_ENV=production 62 | ENV NODE_OPTIONS="--no-node-snapshot" 63 | WORKDIR /app 64 | 65 | COPY package.json app-config*.yaml ./ 66 | COPY --from=build /app /app 67 | 68 | CMD ["packages/backend","--config","app-config.yaml","--config","app-config.production.yaml", "--config","app-config.runtime.yaml"] -------------------------------------------------------------------------------- /plugins/security-champion/src/hooks/useChangeMultipleSecurityChampionsQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { post } from '../api/client'; 3 | import { getBackstageToken } from '../utils/authenticationUtils'; 4 | import { 5 | configApiRef, 6 | identityApiRef, 7 | useApi, 8 | } from '@backstage/core-plugin-api'; 9 | import { SecurityChamp, SecurityChampionBatchUpdate } from '../types'; 10 | 11 | export const useSetMultipleSecurityChampionsMutation = () => { 12 | const backendUrl = useApi(configApiRef).getString('backend.baseUrl'); 13 | const backstageAuthApi = useApi(identityApiRef); 14 | const queryClient = useQueryClient(); 15 | 16 | return useMutation({ 17 | mutationFn: async (securityChampionBatch: SecurityChampionBatchUpdate) => { 18 | const { backstageToken } = await getBackstageToken(backstageAuthApi); 19 | const { email } = await backstageAuthApi.getProfileInfo(); 20 | const userEmail = email ? email : 'no user email'; 21 | 22 | const endpointUrl = `${backendUrl}/api/proxy/security-champion-proxy/api/setSecurityChampions`; 23 | 24 | return post< 25 | { 26 | repositoryNames: string[]; 27 | securityChampionEmail: string; 28 | modifiedBy: string; 29 | }, 30 | string 31 | >(endpointUrl, backstageToken, { 32 | repositoryNames: securityChampionBatch.repositoryNames, 33 | securityChampionEmail: securityChampionBatch.securityChampionEmail, 34 | modifiedBy: userEmail, 35 | }); 36 | }, 37 | onMutate: async (batch: SecurityChampionBatchUpdate) => { 38 | const queryKey = ['security-champions', batch.repositoryNames]; 39 | 40 | await queryClient.cancelQueries({ queryKey }); 41 | 42 | const previousChampions = 43 | queryClient.getQueryData(queryKey); 44 | 45 | queryClient.setQueryData(queryKey, old => { 46 | if (!old || old.length === 0) { 47 | return batch.repositoryNames.map(repo => ({ 48 | repositoryName: repo, 49 | securityChampionEmail: batch.securityChampionEmail, 50 | })); 51 | } 52 | 53 | const updated = old.map(champ => { 54 | if (batch.repositoryNames.includes(champ.repositoryName)) { 55 | return { 56 | ...champ, 57 | securityChampionEmail: batch.securityChampionEmail, 58 | }; 59 | } 60 | return champ; 61 | }); 62 | 63 | const existingRepoNames = old.map(c => c.repositoryName); 64 | const newRepos = batch.repositoryNames 65 | .filter(repo => !existingRepoNames.includes(repo)) 66 | .map(repo => ({ 67 | repositoryName: repo, 68 | securityChampionEmail: batch.securityChampionEmail, 69 | })); 70 | 71 | return [...updated, ...newRepos]; 72 | }); 73 | 74 | return { previousChampions, queryKey }; 75 | }, 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /plugins/security-metrics/src/hooks/useFetchRepositoryNames.ts: -------------------------------------------------------------------------------- 1 | import { useApi } from '@backstage/core-plugin-api'; 2 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 3 | import { 4 | Entity, 5 | RELATION_DEPENDS_ON, 6 | RELATION_HAS_PART, 7 | RELATION_OWNER_OF, 8 | RELATION_PARENT_OF, 9 | } from '@backstage/catalog-model'; 10 | import { getChildRefs } from '../utils/getChildRefs'; 11 | import { isExperimentalLifecycle } from '../components/utils'; 12 | import { useQuery } from '@tanstack/react-query'; 13 | 14 | const REPOSITORY_ENTITY_KIND = 'Component'; 15 | const HIGHER_LEVEL_ENTITIES = ['Group', 'Domain', 'System']; 16 | 17 | const isEntity = (entity: Entity | undefined): entity is Entity => !!entity; 18 | 19 | export const useFetchComponentNamesByGroup = (rootGroupRef: Entity) => { 20 | const rootgroupchildren = getChildRefs([rootGroupRef]); 21 | const catalog = useApi(catalogApiRef); 22 | 23 | const getAllComponentNamesByRecursion = async ( 24 | entityRefs: string[], 25 | repositoryEntities: string[] = [], 26 | visitedRefs: Set = new Set(), 27 | ): Promise => { 28 | const newGroupRefs = entityRefs.filter(ref => !visitedRefs.has(ref)); 29 | if (newGroupRefs.length === 0) return repositoryEntities; 30 | 31 | newGroupRefs.forEach(ref => visitedRefs.add(ref)); 32 | 33 | const resultEntities = ( 34 | await catalog.getEntitiesByRefs({ 35 | entityRefs: newGroupRefs, 36 | }) 37 | ).items.filter(isEntity); 38 | 39 | const childrenRefs: string[] = []; 40 | 41 | resultEntities.forEach(item => { 42 | if ( 43 | item.kind === REPOSITORY_ENTITY_KIND && 44 | !isExperimentalLifecycle(item.spec?.lifecycle) 45 | ) { 46 | repositoryEntities.push(item.metadata.name); 47 | } else if (HIGHER_LEVEL_ENTITIES.includes(item.kind)) { 48 | item.relations?.forEach(relation => { 49 | if ( 50 | [ 51 | RELATION_OWNER_OF, 52 | RELATION_HAS_PART, 53 | RELATION_PARENT_OF, 54 | RELATION_DEPENDS_ON, 55 | ].includes(relation.type) 56 | ) { 57 | childrenRefs.push(relation.targetRef); 58 | } 59 | }); 60 | } 61 | }); 62 | 63 | if (!childrenRefs || childrenRefs.length === 0) { 64 | return repositoryEntities; 65 | } 66 | 67 | return getAllComponentNamesByRecursion( 68 | childrenRefs, 69 | repositoryEntities, 70 | visitedRefs, 71 | ); 72 | }; 73 | 74 | const { data, isPending, error } = useQuery({ 75 | queryKey: ['group-components', rootGroupRef], 76 | queryFn: async () => { 77 | const names = await getAllComponentNamesByRecursion(rootgroupchildren); 78 | return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b)); 79 | }, 80 | }); 81 | 82 | return { 83 | componentNames: data ?? [], 84 | componentNamesIsLoading: isPending, 85 | componentNamesError: error, 86 | }; 87 | }; 88 | --------------------------------------------------------------------------------