diff --git a/examples/entities.yaml b/examples/entities.yaml
index 359a1da..1e2413e 100644
--- a/examples/entities.yaml
+++ b/examples/entities.yaml
@@ -12,11 +12,13 @@ apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-website
+ namespace: default
annotations:
aws.amazon.com/aws-codepipeline-tags: component=example-website
aws.amazon.com/aws-codebuild-project-tags: component=example-website
aws.amazon.com/amazon-ecs-service-tags: component=example-website
aws.amazon.com/cost-insights-tags: component=example-website
+ aws.amazon.com/aws-ecr-repository-arn: 1234567890.dkr.ecr.us-east-1.amazonaws.com/example-website
spec:
type: website
lifecycle: experimental
diff --git a/packages/app/package.json b/packages/app/package.json
index 23f8c7c..27bfc6b 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -18,6 +18,7 @@
"@aws/aws-codebuild-plugin-for-backstage": "workspace:^",
"@aws/aws-codepipeline-plugin-for-backstage": "workspace:^",
"@aws/cost-insights-plugin-for-backstage": "workspace:^",
+ "@aws/ecr-plugin-for-backstage": "workspace:^",
"@backstage-community/plugin-cost-insights": "^0.12.25",
"@backstage-community/plugin-github-actions": "^0.6.16",
"@backstage-community/plugin-tech-radar": "^0.7.4",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index efc0bc6..4b84883 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -57,6 +57,11 @@ import {
} from '@aws/aws-codepipeline-plugin-for-backstage';
import { EntityAwsCodeBuildCard } from '@aws/aws-codebuild-plugin-for-backstage';
import { EntityAmazonEcsServicesContent } from '@aws/amazon-ecs-plugin-for-backstage';
+import {
+ isAwsEcrScanResultsAvailable,
+ EntityEcrScanResultsContent,
+} from '@aws/ecr-plugin-for-backstage';
+
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
@@ -216,6 +221,10 @@ const websiteEntityPage = (
+
+
+
+
diff --git a/packages/backend/package.json b/packages/backend/package.json
index ef339fc..43c8002 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -21,6 +21,7 @@
"@aws/aws-codepipeline-plugin-for-backstage-backend": "workspace:^",
"@aws/aws-core-plugin-for-backstage-scaffolder-actions": "workspace:^",
"@aws/cost-insights-plugin-for-backstage-backend": "workspace:^",
+ "@aws/ecr-plugin-for-backstage-backend": "workspace:^",
"@backstage/backend-defaults": "^0.5.3",
"@backstage/backend-plugin-api": "^1.0.2",
"@backstage/catalog-client": "^1.8.0",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 26c7929..eb6664b 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -17,6 +17,7 @@ backend.add(import('@aws/amazon-ecs-plugin-for-backstage-backend'));
backend.add(import('@aws/aws-codebuild-plugin-for-backstage-backend'));
backend.add(import('@aws/aws-codepipeline-plugin-for-backstage-backend'));
backend.add(import('@aws/aws-core-plugin-for-backstage-scaffolder-actions'));
+backend.add(import('@aws/ecr-plugin-for-backstage-backend'));
backend.add(import('@aws/cost-insights-plugin-for-backstage-backend'));
diff --git a/plugins/ecr/README.md b/plugins/ecr/README.md
new file mode 100644
index 0000000..7a4722b
--- /dev/null
+++ b/plugins/ecr/README.md
@@ -0,0 +1,116 @@
+# ECR AWS plugin for Backstage
+
+This plugin is meant to allow you to view ECR Scan Results for a specific entity within your Backstage UI.
+
+This requires that where you run Backstage has AWS Credentials that has IAM Permissions to describe images and get ECR scan findings (through enviornment variables, IRSA, etc;).
+
+The plugin consists of the following packages:
+
+- `frontend`: The frontend plugin package installed in Backstage
+- `backend`: The backend plugin package installed in Backstage
+- `common`: Types and utilities shared between the packages
+
+## Installing
+
+This guide assumes that you are familiar with the general [Getting Started](../../docs/getting-started.md) documentation and have assumes you have an existing Backstage application.
+
+### Permissions
+
+The IAM role(s) used by Backstage will require the following permissions:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ecr:DescribeImages",
+ "ecr:DescribeImageScanFindings"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+```
+
+Note: This policy does not reflect least privilege and you should further limit the policy to the appropriate AWS resources.
+
+### Backend package
+
+Install the backend package in your Backstage app:
+
+```shell
+yarn workspace backend add @aws/ecr-plugin-for-backstage-backend
+```
+
+Add the plugin to the `packages/backend/src/index.ts`:
+
+```typescript
+const backend = createBackend();
+// ...
+backend.add(import('@aws/ecr-plugin-for-backstage-backend'));
+// ...
+backend.start();
+```
+
+### Frontend package
+
+Install the frontend packages in your Backstage app:
+
+```shell
+yarn workspace app add @aws/ecr-plugin-for-backstage
+```
+Edit the `packages/app/src/components/catalog/EntityPage.tsx` and add the imports
+
+```typescript jsx
+import {
+ EntityEcrScanResultsContent,
+ isAwsEcrScanResultsAvailable
+} from 'plugin-aws-ecr-scan';
+```
+
+Then add the following components:
+
+```typescript jsx
+
+
+
+```
+
+## Entity annotations
+
+The plugin uses entity annotations to determine what queries to make for a given entity. The `aws.amazon.com/aws-ecr-repository-arn` annotation can be added to any catalog entity to attach an ECR Repository to the entity.
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ # ...
+ annotations:
+ aws.amazon.com/aws-ecr-repository-arn: 1234567890.dkr.ecr.us-east-1.amazonaws.com/example-website
+spec:
+ type: service
+ # ...
+```
+
+## Configuration
+
+There are configuration options available to control the behavior of the plugin.
+
+```yaml
+aws:
+ ecr:
+ accountId: '1111111111' # (Optional) Use the specified AWS account ID
+ region: 'us-west-2' # (Optional) Use the specified AWS region
+ maxImages: 1000 # (Optional) The maximum amount of images grabbed for a repository.
+ maxScanFindings: 1000 # (Optional) The maximum amount of scan findings grabbed for an individual image.
+ cache:
+ enable: true # (Optional) Control is caching is enabled, defaults to true
+ defaultTtl: 1000 # (Optional) How long responses are cached for in milliseconds, defaults to 1 day
+ readTime: 1000 # (Optional) Read timeout when operating with cache in milliseconds, defaults to 1000 ms
+```
+
+### Caching
+
+By default the responses from the backend plugin are cached for 1 day in order to improve performance to the user for querying the ECR API. The cache TTL can be configured to a different value or alternatively it can be disabled entirely if needed.
diff --git a/plugins/ecr/backend/.eslintrc.js b/plugins/ecr/backend/.eslintrc.js
new file mode 100644
index 0000000..e2a53a6
--- /dev/null
+++ b/plugins/ecr/backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/ecr/backend/README.md b/plugins/ecr/backend/README.md
new file mode 100644
index 0000000..cb3e080
--- /dev/null
+++ b/plugins/ecr/backend/README.md
@@ -0,0 +1,3 @@
+# ECR AWS plugin for Backstage - Backend
+
+This a backend plugin package for Backstage related to ECR for AWS. For more information see the [documentation](../README.md).
diff --git a/plugins/ecr/backend/dev/index.ts b/plugins/ecr/backend/dev/index.ts
new file mode 100644
index 0000000..40908dd
--- /dev/null
+++ b/plugins/ecr/backend/dev/index.ts
@@ -0,0 +1,8 @@
+import { createBackend } from '@backstage/backend-defaults';
+
+const backend = createBackend();
+backend.add(import('@backstage/plugin-auth-backend'));
+backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
+backend.add(import('../src'));
+
+backend.start();
diff --git a/plugins/ecr/backend/package.json b/plugins/ecr/backend/package.json
new file mode 100644
index 0000000..cc4f105
--- /dev/null
+++ b/plugins/ecr/backend/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "@aws/ecr-plugin-for-backstage-backend",
+ "version": "0.1.0",
+ "repository": {
+ "type": "git",
+ "url": "github:awslabs/backstage-plugins-for-aws",
+ "directory": "plugins/ecs/backend"
+ },
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin",
+ "pluginId": "ecr-aws",
+ "pluginPackages": [
+ "@aws/ecr-plugin-for-backstage",
+ "@aws/ecr-plugin-for-backstage-backend"
+ ]
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@aws-sdk/client-ecr": "^3.568.0",
+ "@aws-sdk/middleware-sdk-sts": "^3.523.0",
+ "@aws-sdk/types": "^3.609.0",
+ "@aws-sdk/util-arn-parser": "^3.495.0",
+ "@aws/aws-core-plugin-for-backstage-common": "workspace:^",
+ "@backstage/backend-common": "^0.25.0",
+ "@backstage/backend-plugin-api": "^1.0.2",
+ "@backstage/catalog-client": "^1.8.0",
+ "@backstage/catalog-model": "^1.7.1",
+ "@backstage/config": "^1.3.0",
+ "@backstage/errors": "^1.2.5",
+ "@backstage/integration-aws-node": "^0.1.13",
+ "@backstage/plugin-catalog-node": "^1.14.0",
+ "@types/express": "*",
+ "express": "^4.17.1",
+ "express-promise-router": "^4.1.0",
+ "luxon": "^3.4.4",
+ "node-fetch": "^2.6.7",
+ "regression": "^2.0.1",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-defaults": "^0.5.3",
+ "@backstage/backend-test-utils": "^1.1.0",
+ "@backstage/cli": "^0.29.2",
+ "@backstage/plugin-auth-backend": "^0.24.0",
+ "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.2",
+ "@types/luxon": "^3",
+ "@types/regression": "^2",
+ "@types/supertest": "^2.0.12",
+ "aws-sdk-client-mock": "^4.0.0",
+ "msw": "^1.0.0",
+ "supertest": "^6.2.4"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/ecr/backend/src/cache/ECRCache.ts b/plugins/ecr/backend/src/cache/ECRCache.ts
new file mode 100644
index 0000000..f67ef99
--- /dev/null
+++ b/plugins/ecr/backend/src/cache/ECRCache.ts
@@ -0,0 +1,78 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { assertError } from '@backstage/errors';
+import { CacheService, LoggerService } from '@backstage/backend-plugin-api';
+import { EcrConfig } from '../config';
+
+const KEY_PREFIX = 'aws-ecr-provider:';
+
+export class EcrCache {
+ protected readonly cache: CacheService;
+ protected readonly logger: LoggerService;
+ protected readonly readTimeout: number;
+
+ private constructor({
+ cache,
+ logger,
+ readTimeout,
+ }: {
+ cache: CacheService;
+ logger: LoggerService;
+ readTimeout: number;
+ }) {
+ this.cache = cache;
+ this.logger = logger;
+ this.readTimeout = readTimeout;
+ }
+
+ static fromConfig(
+ config: EcrConfig,
+ { cache, logger }: { cache: CacheService; logger: LoggerService },
+ ) {
+ return new EcrCache({
+ cache: cache.withOptions({ defaultTtl: config.cache.defaultTtl }),
+ logger,
+ readTimeout: config.cache.readTimeout,
+ });
+ }
+
+ async get(path: string): Promise {
+ try {
+ const response = (await Promise.race([
+ this.cache.get(`${KEY_PREFIX}${path}`),
+ new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)),
+ ])) as string | undefined;
+
+ if (response !== undefined) {
+ this.logger.debug(`Cache hit: ${path}`);
+ return response;
+ }
+
+ this.logger.debug(`Cache miss: ${path}`);
+ return response;
+ } catch (e) {
+ assertError(e);
+ this.logger.warn(`Error getting cache entry ${path}: ${e.message}`);
+ this.logger.debug(e.stack || '-');
+ return undefined;
+ }
+ }
+
+ async set(path: string, data: string): Promise {
+ this.logger.debug(`Writing cache entry for ${path}`);
+ this.cache
+ .set(`${KEY_PREFIX}${path}`, data)
+ .catch(e => this.logger.error('write error', e));
+ }
+}
diff --git a/plugins/ecr/backend/src/cache/index.ts b/plugins/ecr/backend/src/cache/index.ts
new file mode 100644
index 0000000..9113051
--- /dev/null
+++ b/plugins/ecr/backend/src/cache/index.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './ECRCache';
diff --git a/plugins/ecr/backend/src/config/config.ts b/plugins/ecr/backend/src/config/config.ts
new file mode 100644
index 0000000..343b0fc
--- /dev/null
+++ b/plugins/ecr/backend/src/config/config.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config } from '@backstage/config';
+import {
+ EcrAwsConfig,
+ EcrConfig,
+ EcrAwsConfigCache,
+} from './types';
+
+export function readEcrConfig(
+ config: Config,
+): EcrConfig {
+ const root = config.getOptionalConfig('aws.ecr');
+
+ return {
+ ecr: readEcrAwsConfig(root),
+ cache: readEcrAwsConfigCache(root),
+ };
+}
+
+function readEcrAwsConfig(
+ config: Config | undefined,
+): EcrAwsConfig {
+
+ return {
+ accountId: config?.getOptionalString('accountId'),
+ region: config?.getOptionalString('region'),
+ maxImages: config?.getOptionalNumber('maxImages'),
+ maxScanFindings: config?.getOptionalNumber('maxScanFindings'),
+ };
+}
+
+function readEcrAwsConfigCache(
+ config: Config | undefined,
+): EcrAwsConfigCache {
+ const root = config?.getOptionalConfig('cache');
+
+ return {
+ enable: root?.getOptionalBoolean('enable') || true,
+ defaultTtl: root?.getOptionalNumber('defaultTtl') || 86400000,
+ readTimeout: root?.getOptionalNumber('readTimeout') || 1000,
+ };
+}
diff --git a/plugins/ecr/backend/src/config/index.ts b/plugins/ecr/backend/src/config/index.ts
new file mode 100644
index 0000000..34a4b7c
--- /dev/null
+++ b/plugins/ecr/backend/src/config/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './config';
+export * from './types';
diff --git a/plugins/ecr/backend/src/config/types.ts b/plugins/ecr/backend/src/config/types.ts
new file mode 100644
index 0000000..c9c4f0c
--- /dev/null
+++ b/plugins/ecr/backend/src/config/types.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type EcrConfig = {
+ cache: EcrAwsConfigCache;
+ ecr: EcrAwsConfig;
+};
+
+export type EcrAwsConfigCache = {
+ enable: boolean;
+ defaultTtl: number;
+ readTimeout: number;
+};
+
+export type EcrAwsConfig = {
+ accountId: string | undefined;
+ region: string | undefined;
+ maxImages: number | undefined;
+ maxScanFindings: number | undefined;
+};
diff --git a/plugins/ecr/backend/src/index.ts b/plugins/ecr/backend/src/index.ts
new file mode 100644
index 0000000..0a0c8c9
--- /dev/null
+++ b/plugins/ecr/backend/src/index.ts
@@ -0,0 +1,16 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './service/router';
+export { ecrAwsPlugin as default } from './plugin';
+export * from "./service/types";
\ No newline at end of file
diff --git a/plugins/ecr/backend/src/plugin.ts b/plugins/ecr/backend/src/plugin.ts
new file mode 100644
index 0000000..854ed49
--- /dev/null
+++ b/plugins/ecr/backend/src/plugin.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createBackendPlugin,
+ coreServices,
+} from '@backstage/backend-plugin-api';
+import { createRouter, ecrAwsServiceRef } from './service/router';
+import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
+import { readEcrConfig } from './config';
+
+export const ecrAwsPlugin = createBackendPlugin({
+ pluginId: 'ecr-aws',
+ register(env) {
+ env.registerInit({
+ deps: {
+ logger: coreServices.logger,
+ httpRouter: coreServices.httpRouter,
+ config: coreServices.rootConfig,
+ catalogApi: catalogServiceRef,
+ auth: coreServices.auth,
+ discovery: coreServices.discovery,
+ httpAuth: coreServices.httpAuth,
+ cache: coreServices.cache,
+ ecrAwsService: ecrAwsServiceRef,
+ },
+ async init({
+ logger,
+ httpRouter,
+ config,
+ auth,
+ httpAuth,
+ discovery,
+ cache,
+ ecrAwsService,
+ }) {
+ const pluginConfig = readEcrConfig(config);
+
+ httpRouter.use(
+ await createRouter({
+ logger,
+ ecrAwsService,
+ discovery,
+ auth,
+ httpAuth,
+ cache,
+ config: pluginConfig,
+ }),
+ );
+ httpRouter.addAuthPolicy({
+ path: '/health',
+ allow: 'unauthenticated',
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/ecr/backend/src/service/EcrAwsService.ts b/plugins/ecr/backend/src/service/EcrAwsService.ts
new file mode 100644
index 0000000..de37e77
--- /dev/null
+++ b/plugins/ecr/backend/src/service/EcrAwsService.ts
@@ -0,0 +1,233 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ECRClient,
+ DescribeImageScanFindingsCommand,
+ ImageScanFindings,
+ DescribeImagesCommand,
+ ImageDetail,
+} from '@aws-sdk/client-ecr';
+import { CatalogApi } from '@backstage/catalog-client';
+import {
+ AWS_SDK_CUSTOM_USER_AGENT,
+ getOneOfEntityAnnotations,
+} from '@aws/aws-core-plugin-for-backstage-common';
+import {
+ AwsEcrListImagesRequest,
+ AwsEcrListImagesResponse,
+ AwsEcrListScanResultsRequest,
+ AwsEcrListScanResultsResponse,
+ EcrScanAwsService,
+} from './types';
+import {
+ AwsCredentialsManager,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
+import {
+ AuthService,
+ BackstageCredentials,
+ coreServices,
+ createServiceFactory,
+ createServiceRef,
+ DiscoveryService,
+ HttpAuthService,
+ LoggerService,
+} from '@backstage/backend-plugin-api';
+import { createLegacyAuthAdapters } from '@backstage/backend-common';
+import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
+import { EcrConfig, readEcrConfig } from '../config';
+import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
+import { stringifyEntityRef } from '@backstage/catalog-model';
+
+export const ECR_ARN_ANNOTATION = 'aws.amazon.com/aws-ecr-repository-arn';
+
+export class EcrAwsService
+ implements EcrScanAwsService
+{
+ public constructor(
+ private readonly logger: LoggerService,
+ private readonly auth: AuthService,
+ private readonly catalogApi: CatalogApi,
+ private readonly ecrClient: ECRClient,
+ private readonly config: EcrConfig,
+ ) {}
+
+ static async fromConfig(
+ config: EcrConfig,
+ options: {
+ catalogApi: CatalogApi;
+ discovery: DiscoveryService;
+ auth?: AuthService;
+ httpAuth?: HttpAuthService;
+ logger: LoggerService;
+ credentialsManager: AwsCredentialsManager;
+ },
+ ) {
+ const { auth } = createLegacyAuthAdapters(options);
+
+ const { region, accountId } = config.ecr;
+
+ const { credentialsManager } = options;
+
+ let credentialProvider: AwsCredentialIdentityProvider;
+
+ if (accountId) {
+ credentialProvider = (
+ await credentialsManager.getCredentialProvider({ accountId })
+ ).sdkCredentialProvider;
+ } else {
+ credentialProvider = (await credentialsManager.getCredentialProvider())
+ .sdkCredentialProvider;
+ }
+
+ const ecrClient = new ECRClient({
+ region: region,
+ customUserAgent: AWS_SDK_CUSTOM_USER_AGENT,
+ credentialDefaultProvider: () => credentialProvider,
+ });
+
+ return new EcrAwsService(
+ options.logger,
+ auth,
+ options.catalogApi,
+ ecrClient,
+ config,
+ );
+ }
+ async listEcrImages(
+ req: AwsEcrListImagesRequest,
+ ): Promise {
+ const entity = await this.catalogApi.getEntityByRef(
+ req.entityRef,
+ req.credentials &&
+ (await this.auth.getPluginRequestToken({
+ onBehalfOf: req.credentials,
+ targetPluginId: 'catalog',
+ })),
+ );
+
+ if (!entity) {
+ throw new Error(
+ `Couldn't find entity with name: ${stringifyEntityRef(
+ req.entityRef,
+ )}`,
+ );
+ }
+
+ const arnAnnotation = getOneOfEntityAnnotations(entity, [
+ ECR_ARN_ANNOTATION,
+ ]);
+
+ const images = await this.ecrClient.send(
+ new DescribeImagesCommand({
+ repositoryName: this.extractRepoName(arnAnnotation?.value as string),
+ maxResults: 1000,
+ }),
+ );
+
+ return {
+ items: images.imageDetails as ImageDetail[],
+ };
+ }
+
+ async listScanResults(
+ req: AwsEcrListScanResultsRequest,
+ ): Promise {
+ const entity = await this.catalogApi.getEntityByRef(
+ req.entityRef,
+ req.credentials &&
+ (await this.auth.getPluginRequestToken({
+ onBehalfOf: req.credentials,
+ targetPluginId: 'catalog',
+ })),
+ );
+
+ if (!entity) {
+ throw new Error(
+ `Couldn't find entity with name: ${stringifyEntityRef(
+ req.entityRef,
+ )}`,
+ );
+ }
+
+ const arnAnnotation = getOneOfEntityAnnotations(entity, [
+ ECR_ARN_ANNOTATION,
+ ]);
+ const results = await this.ecrClient.send(
+ new DescribeImageScanFindingsCommand({
+ imageId: {
+ imageDigest: req.imageDigest,
+ imageTag: req.imageTag,
+ },
+ repositoryName: this.extractRepoName(arnAnnotation?.value as string),
+ maxResults: this.config.ecr.maxScanFindings,
+ }),
+ );
+ return {
+ results: results.imageScanFindings as ImageScanFindings
+ }
+ }
+
+ extractRepoName(ecrArn: string): string {
+ // Match the part after the last slash '/' in the arn
+ const match = ecrArn.match(/(?:\.amazonaws\.com\/)(.*)$/);
+ if (match) {
+ return match[1]; // This will return repository name
+ }
+ return ''; // If no match is found
+ }
+
+}
+
+export const ecrAwsServiceRef =
+ createServiceRef({
+ id: 'ecr-aws.api',
+ defaultFactory: async service =>
+ createServiceFactory({
+ service,
+ deps: {
+ logger: coreServices.logger,
+ config: coreServices.rootConfig,
+ catalogApi: catalogServiceRef,
+ auth: coreServices.auth,
+ discovery: coreServices.discovery,
+ httpAuth: coreServices.httpAuth,
+ },
+ async factory({
+ logger,
+ config,
+ catalogApi,
+ auth,
+ httpAuth,
+ discovery,
+ }) {
+ const pluginConfig = readEcrConfig(config);
+
+ const impl = await EcrAwsService.fromConfig(
+ pluginConfig,
+ {
+ catalogApi,
+ auth,
+ httpAuth,
+ discovery,
+ logger,
+ credentialsManager:
+ DefaultAwsCredentialsManager.fromConfig(config),
+ },
+ );
+
+ return impl;
+ },
+ }),
+ });
diff --git a/plugins/ecr/backend/src/service/index.ts b/plugins/ecr/backend/src/service/index.ts
new file mode 100644
index 0000000..b4e9a9c
--- /dev/null
+++ b/plugins/ecr/backend/src/service/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { EcrAwsService } from './EcrAwsService';
+export * from './types';
diff --git a/plugins/ecr/backend/src/service/router.ts b/plugins/ecr/backend/src/service/router.ts
new file mode 100644
index 0000000..557c41f
--- /dev/null
+++ b/plugins/ecr/backend/src/service/router.ts
@@ -0,0 +1,182 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createLegacyAuthAdapters,
+ errorHandler,
+} from '@backstage/backend-common';
+import express from 'express';
+import Router from 'express-promise-router';
+import { EcrScanAwsService } from './types';
+import {
+ AuthService,
+ CacheService,
+ coreServices,
+ createServiceFactory,
+ createServiceRef,
+ DiscoveryService,
+ HttpAuthService,
+ LoggerService,
+} from '@backstage/backend-plugin-api';
+import { EcrCache } from '../cache';
+import { EcrConfig, readEcrConfig } from '../config';
+import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
+import { EcrAwsService } from './EcrAwsService';
+import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node';
+
+export interface RouterOptions {
+ logger: LoggerService;
+ ecrAwsService: EcrScanAwsService;
+ discovery: DiscoveryService;
+ auth?: AuthService;
+ httpAuth?: HttpAuthService;
+ cache: CacheService;
+ config: EcrConfig;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger, ecrAwsService, config, cache } = options;
+
+ const router = Router();
+ router.use(express.json());
+
+ const { httpAuth } = createLegacyAuthAdapters(options);
+
+ let cacheClient: EcrCache | undefined;
+ if (config.cache.enable) {
+ cacheClient = EcrCache.fromConfig(config, { cache, logger });
+
+ router.use((req, res, next) => {
+ const cacheKey = req.originalUrl;
+
+ logger.debug(`Cache key ${cacheKey}`);
+
+ if (cacheClient) {
+ cacheClient.get(cacheKey).then(e => {
+ if (e) {
+ res.send(JSON.parse(e));
+ } else {
+ const originalJson = res.json;
+ res.json = data => {
+ if (cacheClient) {
+ cacheClient.set(cacheKey, JSON.stringify(data));
+ }
+ return originalJson.call(res, data);
+ };
+ next();
+ }
+ });
+ }
+ });
+ }
+
+ router.get('/v1/entity/:namespace/:kind/:name/images', async (req, res) => {
+ try {
+ const { namespace, kind, name } = req.params;
+ logger.debug(`Grabbing Images for ${namespace}:${kind}:${name}`)
+ const images = await ecrAwsService.listEcrImages(
+ {
+ entityRef: {
+ namespace,
+ kind,
+ name,
+ },
+ credentials: await httpAuth.credentials(req),
+ })
+ res.json(images);
+ } catch (error) {
+ logger.error(error)
+ res.json({
+ error: error,
+ })
+ }
+ })
+
+ router.get('/v1/entity/:namespace/:kind/:name/results', async (req, res) => {
+ try {
+ const imageTag = req.query?.imageTag as string;
+ const { namespace, kind, name } = req.params;
+ logger.debug(`Grabbing Scan Results for ${namespace}:${kind}:${name}:${imageTag}`)
+ const scanResults = await ecrAwsService.listScanResults(
+ {
+ entityRef: {
+ namespace,
+ kind,
+ name,
+ },
+ credentials: await httpAuth.credentials(req),
+ imageTag,
+ })
+ res.json(scanResults);
+ } catch (error) {
+ logger.error(error)
+ res.json({
+ error: error,
+ })
+ }
+ })
+
+ router.get('/health', (_, response) => {
+ logger.info('PONG!');
+ response.json({ status: 'ok' });
+ });
+ router.use(errorHandler());
+ return router;
+}
+
+export const ecrAwsServiceRef =
+ createServiceRef({
+ id: 'ecr-aws.api',
+ defaultFactory: async service =>
+ createServiceFactory({
+ service,
+ deps: {
+ logger: coreServices.logger,
+ config: coreServices.rootConfig,
+ catalogApi: catalogServiceRef,
+ auth: coreServices.auth,
+ discovery: coreServices.discovery,
+ httpAuth: coreServices.httpAuth,
+ },
+ async factory({
+ logger,
+ config,
+ catalogApi,
+ auth,
+ httpAuth,
+ discovery,
+ }) {
+ const pluginConfig = readEcrConfig(config);
+
+ const impl = await EcrAwsService.fromConfig(
+ pluginConfig,
+ {
+ catalogApi,
+ auth,
+ httpAuth,
+ discovery,
+ logger,
+ credentialsManager:
+ DefaultAwsCredentialsManager.fromConfig(config),
+ },
+ );
+
+ return impl;
+ },
+ }),
+ });
+
+
+export * from './EcrAwsService';
diff --git a/plugins/ecr/backend/src/service/types.ts b/plugins/ecr/backend/src/service/types.ts
new file mode 100644
index 0000000..156f284
--- /dev/null
+++ b/plugins/ecr/backend/src/service/types.ts
@@ -0,0 +1,52 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import {
+ ImageDetail,
+ ImageScanFindings,
+} from "@aws-sdk/client-ecr"
+import { BackstageCredentials } from "@backstage/backend-plugin-api/index";
+import { CompoundEntityRef } from '@backstage/catalog-model';
+
+/** @public */
+export type AwsEcrListImagesRequest = {
+ entityRef: CompoundEntityRef;
+ credentials?: BackstageCredentials;
+};
+
+/** @public */
+export type AwsEcrListImagesResponse = {
+ items: ImageDetail[];
+};
+
+export type AwsEcrListScanResultsRequest = {
+ entityRef: CompoundEntityRef;
+ credentials?: BackstageCredentials;
+ imageTag?: string;
+ imageDigest?: string;
+};
+
+export type AwsEcrListScanResultsResponse = {
+ results: ImageScanFindings
+};
+
+/** @public */
+export interface EcrScanAwsService {
+ listEcrImages(
+ req: AwsEcrListImagesRequest,
+ ): Promise;
+ listScanResults(
+ req: AwsEcrListScanResultsRequest,
+ ): Promise;
+}
\ No newline at end of file
diff --git a/plugins/ecr/backend/src/setupTests.ts b/plugins/ecr/backend/src/setupTests.ts
new file mode 100644
index 0000000..19d0e85
--- /dev/null
+++ b/plugins/ecr/backend/src/setupTests.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/plugins/ecr/frontend/.eslintrc.js b/plugins/ecr/frontend/.eslintrc.js
new file mode 100644
index 0000000..e2a53a6
--- /dev/null
+++ b/plugins/ecr/frontend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/ecr/frontend/README.md b/plugins/ecr/frontend/README.md
new file mode 100644
index 0000000..07fb735
--- /dev/null
+++ b/plugins/ecr/frontend/README.md
@@ -0,0 +1,3 @@
+# ECR AWS plugin for Backstage - Frontend
+
+This a frontend plugin package for Backstage related to ECR for AWS. For more information see the [documentation](../README.md).
diff --git a/plugins/ecr/frontend/dev/index.tsx b/plugins/ecr/frontend/dev/index.tsx
new file mode 100644
index 0000000..35878ce
--- /dev/null
+++ b/plugins/ecr/frontend/dev/index.tsx
@@ -0,0 +1,17 @@
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createDevApp } from '@backstage/dev-utils';
+import { costInsightsAwsPlugin } from '../src/plugin';
+
+createDevApp().registerPlugin(costInsightsAwsPlugin).render();
diff --git a/plugins/ecr/frontend/package.json b/plugins/ecr/frontend/package.json
new file mode 100644
index 0000000..3bfc3d4
--- /dev/null
+++ b/plugins/ecr/frontend/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@aws/ecr-plugin-for-backstage",
+ "version": "0.2.0",
+ "repository": {
+ "type": "git",
+ "url": "github:awslabs/backstage-plugins-for-aws",
+ "directory": "plugins/ecs/frontend"
+ },
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin",
+ "pluginId": "ecr-aws",
+ "pluginPackages": [
+ "@aws/ecr-plugin-for-backstage",
+ "@aws/ecr-plugin-for-backstage-backend"
+ ]
+ },
+ "sideEffects": false,
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@aws-sdk/client-ecr": "^3.568.0",
+ "@aws-sdk/util-arn-parser": "^3.495.0",
+ "@aws/aws-core-plugin-for-backstage-common": "workspace:^",
+ "@aws/aws-core-plugin-for-backstage-react": "workspace:^",
+ "@aws/ecr-plugin-for-backstage-backend": "workspace:^",
+ "@backstage/catalog-model": "^1.7.1",
+ "@backstage/core-components": "^0.16.1",
+ "@backstage/core-plugin-api": "^1.10.1",
+ "@backstage/errors": "^1.2.5",
+ "@backstage/plugin-catalog-react": "^1.14.2",
+ "@backstage/theme": "^0.6.2",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.61",
+ "dateformat": "^5.0.3",
+ "humanize-duration": "^3.31.0",
+ "react-use": "^17.2.4",
+ "recharts": "^2.12.7"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.29.2",
+ "@backstage/core-app-api": "^1.15.2",
+ "@backstage/dev-utils": "^1.1.4",
+ "@backstage/test-utils": "^1.7.2",
+ "@testing-library/dom": "^9.0.0",
+ "@testing-library/jest-dom": "^6.0.0",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/dateformat": "^5",
+ "@types/humanize-duration": "^3",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/ecr/frontend/src/api/AwsEcrApi.ts b/plugins/ecr/frontend/src/api/AwsEcrApi.ts
new file mode 100644
index 0000000..419a12e
--- /dev/null
+++ b/plugins/ecr/frontend/src/api/AwsEcrApi.ts
@@ -0,0 +1,17 @@
+import { createApiRef } from '@backstage/core-plugin-api';
+import { AwsEcrListImagesRequest, AwsEcrListImagesResponse, AwsEcrListScanResultsRequest, AwsEcrListScanResultsResponse } from '@aws/ecr-plugin-for-backstage-backend';
+
+/** @public */
+export const awsEcrScanApiRef = createApiRef({
+ id: 'plugin.aws-ecr-scan.service',
+});
+
+/** @public */
+export interface AwsEcrScanApi {
+ listEcrImages(
+ req: AwsEcrListImagesRequest,
+ ): Promise;
+ listScanResults(
+ req: AwsEcrListScanResultsRequest,
+ ): Promise;
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/api/AwsEcrClient.ts b/plugins/ecr/frontend/src/api/AwsEcrClient.ts
new file mode 100644
index 0000000..11590c2
--- /dev/null
+++ b/plugins/ecr/frontend/src/api/AwsEcrClient.ts
@@ -0,0 +1,75 @@
+import { DiscoveryApi, IdentityApi } from "@backstage/core-plugin-api";
+import { AwsEcrScanApi } from "./AwsEcrApi";
+
+export class AwsEcrClient implements AwsEcrScanApi {
+ private readonly discoveryApi: DiscoveryApi;
+ private readonly identityApi: IdentityApi;
+
+ public constructor(options: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ }) {
+ this.discoveryApi = options.discoveryApi;
+ this.identityApi = options.identityApi;
+ }
+ public async listEcrImages(req: any): Promise {
+ try {
+ const urlSegment = `v1/entity/${encodeURIComponent(
+ req.entityRef.namespace,
+ )}/${encodeURIComponent(req.entityRef.kind)}/${encodeURIComponent(
+ req.entityRef.name,
+ )}/images`;
+
+
+ const items = await this.get(urlSegment);
+ if (!items.items) {
+ return { items: [] };
+ }
+ return {
+ items: items.items
+ };
+ } catch (error) {
+ console.error(error);
+ return { items: [] };
+ }
+ }
+ public async listScanResults(req: any): Promise {
+ try {
+ const queryString = new URLSearchParams();
+ queryString.append("imageTag", req.imageTag as string)
+ const urlSegment = `v1/entity/${encodeURIComponent(
+ req.entityRef.namespace,
+ )}/${encodeURIComponent(req.entityRef.kind)}/${encodeURIComponent(
+ req.entityRef.name,
+ )}/results?${queryString}`;
+
+ const results = await this.get(urlSegment);
+ if (!results.results) {
+ return { results: {} };
+ }
+ return {
+ results: results.results
+ };
+ } catch (error) {
+ console.error(error);
+ return { results: {} };
+ }
+ }
+
+ private async get(path: string): Promise {
+ const baseUrl = `${await this.discoveryApi.getBaseUrl('ecr-aws')}/`;
+ const url = new URL(path, baseUrl);
+
+ const { token: idToken } = await this.identityApi.getCredentials();
+ const response = await fetch(url.toString(), {
+ headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
+ });
+
+ if (!response.ok) {
+ throw Error(response.statusText);
+ }
+
+ return response.json() as Promise;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/api/index.ts b/plugins/ecr/frontend/src/api/index.ts
new file mode 100644
index 0000000..ad7b6bb
--- /dev/null
+++ b/plugins/ecr/frontend/src/api/index.ts
@@ -0,0 +1,2 @@
+export * from "./AwsEcrApi"
+export * from "./AwsEcrClient"
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/EntityEcrScanResultsContent.tsx b/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/EntityEcrScanResultsContent.tsx
new file mode 100644
index 0000000..dc7280c
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/EntityEcrScanResultsContent.tsx
@@ -0,0 +1,64 @@
+import React, { useEffect, useState } from 'react';
+import { Grid, FormControl, InputLabel, MenuItem, Select, Divider } from '@material-ui/core';
+import {
+ Page,
+ Content,
+ ContentHeader,
+} from '@backstage/core-components';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { useImages } from '../../hooks/useImages';
+import { PieChart } from '../PieChart';
+import { ResultsTable } from '../ResultsTable';
+import { ImageDetail } from '@aws-sdk/client-ecr';
+import { useSearchParams } from 'react-router-dom';
+
+
+export const EntityEcrScanResultsContent = () => {
+ const { entity } = useEntity();
+
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [imageTag, setImageTag] = useState("")
+
+ const handleChange = (event: any) => {
+ setSearchParams({
+ imageTag: event.target.value,
+ })
+ }
+
+ const { images } = useImages(entity)
+
+ useEffect(() => {
+ setImageTag(searchParams.get("imageTag") as string)
+ }, [searchParams])
+
+
+
+ return (
+
+
+
+
+
+ Images
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/index.ts b/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/index.ts
new file mode 100644
index 0000000..354a760
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/EntityEcrScanResultsContent/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { EntityEcrScanResultsContent } from './EntityEcrScanResultsContent';
diff --git a/plugins/ecr/frontend/src/components/PieChart/PieChart.tsx b/plugins/ecr/frontend/src/components/PieChart/PieChart.tsx
new file mode 100644
index 0000000..3961aa9
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/PieChart/PieChart.tsx
@@ -0,0 +1,59 @@
+/* eslint-disable guard-for-in */
+import React, { useState } from 'react';
+import { Content, GaugeCard } from "@backstage/core-components"
+import { Grid } from "@material-ui/core";
+import { Entity, getCompoundEntityRef } from "@backstage/catalog-model";
+import { useApi } from "@backstage/core-plugin-api";
+import { awsEcrScanApiRef } from "../../api";
+
+export const PieChart = (props: { entity: Entity, imageTag: string }) => {
+ const api = useApi(awsEcrScanApiRef);
+
+ const [results, setResults] = useState()
+
+ React.useEffect(() => {
+ async function getRes() {
+ const scanResults = await api.listScanResults({
+ entityRef: getCompoundEntityRef(props.entity),
+ imageTag: props.imageTag,
+ });
+
+ return scanResults
+
+ }
+ if (props.imageTag !== "") {
+ getRes().then(res => setResults(res))
+ }
+ }, [api, props.entity, props.imageTag])
+
+ const data: {severity: string, count: number}[] = []
+ let total = 0
+ for (const severity in results?.results?.findingSeverityCounts) {
+ const count = results?.results.findingSeverityCounts?.[severity] as number
+ const sev = severity as string
+ data.push({
+ severity: sev,
+ count: count
+ })
+ total += count
+ }
+ return (
+
+
+ {data.map(severity => {
+ return(
+
+
+
+ )
+ })}
+
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/components/PieChart/index.ts b/plugins/ecr/frontend/src/components/PieChart/index.ts
new file mode 100644
index 0000000..121f9a3
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/PieChart/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { PieChart } from './PieChart';
diff --git a/plugins/ecr/frontend/src/components/ResultsTable/ResultsTable.tsx b/plugins/ecr/frontend/src/components/ResultsTable/ResultsTable.tsx
new file mode 100644
index 0000000..057734b
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/ResultsTable/ResultsTable.tsx
@@ -0,0 +1,81 @@
+import {
+ Table,
+ TableColumn,
+} from "@backstage/core-components";
+import * as React from 'react';
+import { ImageScanFinding } from "@aws-sdk/client-ecr"
+import { Entity, getCompoundEntityRef } from "@backstage/catalog-model";
+import { useApi } from "@backstage/core-plugin-api";
+import { awsEcrScanApiRef } from "../../api";
+import { Typography } from "@material-ui/core";
+
+export const ResultsTable = (props: {imageTag: string, entity: Entity}) => {
+ const [results, setResults] = React.useState()
+ const [columns, setColumns] = React.useState([
+ {
+ title: 'Name',
+ field: 'name',
+ width: '25%',
+ },
+ { title: 'Severity',
+ field: 'severity',
+ width: '20%',
+ defaultSort: 'desc',
+ },
+ { title: 'Description',
+ field: 'description',
+ width: '30%'
+ },
+ ]);
+ const api = useApi(awsEcrScanApiRef);
+
+ React.useEffect(() => {
+ async function getRes() {
+ const scanResults = await api.listScanResults({
+ entityRef: getCompoundEntityRef(props.entity),
+ imageTag: props.imageTag,
+ });
+
+ return scanResults
+
+ }
+ if (props.imageTag !== "") {
+ getRes().then(res => {
+ if (!!res?.results?.enhancedFindings?.length) {
+ const newColumns = [...columns];
+ newColumns[0].field = "title";
+ setColumns(newColumns);
+ }
+ setResults(res)
+ })
+ }
+ }, [api, props.entity, props.imageTag])
+
+ return !!results?.results ? (
+ <>
+ {!results?.results?.enhancedFindings?.length &&
+ No Findings Yet
+ }
+ {!!results?.results?.enhancedFindings?.length &&
+
+ }
+ >
+ ) : <>>
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/components/ResultsTable/index.ts b/plugins/ecr/frontend/src/components/ResultsTable/index.ts
new file mode 100644
index 0000000..e7a0c12
--- /dev/null
+++ b/plugins/ecr/frontend/src/components/ResultsTable/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { ResultsTable } from './ResultsTable';
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/hooks/useImageScanResults.ts b/plugins/ecr/frontend/src/hooks/useImageScanResults.ts
new file mode 100644
index 0000000..f8e428a
--- /dev/null
+++ b/plugins/ecr/frontend/src/hooks/useImageScanResults.ts
@@ -0,0 +1,27 @@
+import { Entity, getCompoundEntityRef } from '@backstage/catalog-model';
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync } from 'react-use';
+import { awsEcrScanApiRef } from '../api';
+import { ECR_ANNOTATION } from '../plugin';
+
+
+export const useImageScanResults = (
+ entity: Entity,
+ imageTag: string,
+): { results?: any; error?: Error; loading: boolean } => {
+ const api = useApi(awsEcrScanApiRef);
+ const componentKey = entity.metadata?.annotations?.[ECR_ANNOTATION] as string;
+
+ const { value, loading, error } = useAsync(() => {
+ return api.listScanResults({
+ entityRef: getCompoundEntityRef(entity),
+ imageTag: imageTag,
+ });
+ }, [componentKey]);
+
+ return {
+ results: value,
+ loading,
+ error,
+ };
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/hooks/useImages.ts b/plugins/ecr/frontend/src/hooks/useImages.ts
new file mode 100644
index 0000000..2f455cc
--- /dev/null
+++ b/plugins/ecr/frontend/src/hooks/useImages.ts
@@ -0,0 +1,41 @@
+import { Entity, getCompoundEntityRef } from '@backstage/catalog-model';
+import { useApi } from '@backstage/core-plugin-api';
+import { ImageDetail } from '@aws-sdk/client-ecr';
+import { useAsync } from 'react-use';
+import { awsEcrScanApiRef } from '../api';
+import { ECR_ANNOTATION } from '../plugin';
+
+export const useImages = (
+ entity: Entity,
+): { images?: any; error?: Error; loading: boolean } => {
+ const api = useApi(awsEcrScanApiRef);
+ const componentKey = entity.metadata?.annotations?.[ECR_ANNOTATION] as string;
+
+ const { value, loading, error } = useAsync(() => {
+ return api.listEcrImages({
+ entityRef: getCompoundEntityRef(entity),
+ });
+ }, [componentKey]);
+
+ const items = value?.items.sort((a: ImageDetail, b: ImageDetail) => {
+ if (a.imagePushedAt === undefined) {
+ return 1;
+ }
+
+ if (b.imagePushedAt === undefined) {
+ return -1;
+ }
+ const a1 = new Date(a.imagePushedAt).getTime();
+ const b1 = new Date(b.imagePushedAt).getTime();
+
+ return b1 - a1;
+ }) as ImageDetail[]
+
+ return {
+ images: {
+ items,
+ },
+ loading,
+ error,
+ };
+}
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/index.ts b/plugins/ecr/frontend/src/index.ts
new file mode 100644
index 0000000..c78902c
--- /dev/null
+++ b/plugins/ecr/frontend/src/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { awsEcrScanPlugin, EntityEcrScanResultsContent, isAwsEcrScanResultsAvailable } from './plugin';
diff --git a/plugins/ecr/frontend/src/plugin.test.ts b/plugins/ecr/frontend/src/plugin.test.ts
new file mode 100644
index 0000000..c9202e3
--- /dev/null
+++ b/plugins/ecr/frontend/src/plugin.test.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { awsEcrScanPlugin, EntityEcrScanResultsContent } from './plugin';
+
+describe('aws-ecr-scan', () => {
+ it('should export plugin', () => {
+ expect(awsEcrScanPlugin).toBeDefined();
+ });
+ it('should export page', () => {
+ expect(EntityEcrScanResultsContent).toBeDefined();
+ });
+});
diff --git a/plugins/ecr/frontend/src/plugin.ts b/plugins/ecr/frontend/src/plugin.ts
new file mode 100644
index 0000000..199ae30
--- /dev/null
+++ b/plugins/ecr/frontend/src/plugin.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Entity } from '@backstage/catalog-model';
+import {
+ createApiFactory,
+ createPlugin,
+ createComponentExtension,
+ discoveryApiRef,
+ identityApiRef,
+} from '@backstage/core-plugin-api';
+import { AwsEcrClient, awsEcrScanApiRef } from './api';
+
+export const ECR_ANNOTATION = 'aws.amazon.com/aws-ecr-repository-arn';
+
+export const isAwsEcrScanResultsAvailable = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[ECR_ANNOTATION]);
+
+export const awsEcrScanPlugin = createPlugin({
+ id: 'aws-ecr-scan',
+ apis: [
+ createApiFactory({
+ api: awsEcrScanApiRef,
+ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
+ factory: ({ discoveryApi, identityApi }) =>
+ new AwsEcrClient({ discoveryApi, identityApi }),
+ }),
+ ],
+});
+
+export const EntityEcrScanResultsContent = awsEcrScanPlugin.provide(
+ createComponentExtension({
+ name: 'AwsEcrScanTab',
+ component: {
+ lazy: () =>
+ import('./components/EntityEcrScanResultsContent').then(
+ m => m.EntityEcrScanResultsContent,
+ ),
+ },
+ }),
+);
diff --git a/plugins/ecr/frontend/src/routes.ts b/plugins/ecr/frontend/src/routes.ts
new file mode 100644
index 0000000..4fadf85
--- /dev/null
+++ b/plugins/ecr/frontend/src/routes.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createRouteRef } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'ecr-aws',
+});
\ No newline at end of file
diff --git a/plugins/ecr/frontend/src/setupTests.ts b/plugins/ecr/frontend/src/setupTests.ts
new file mode 100644
index 0000000..9bb3e72
--- /dev/null
+++ b/plugins/ecr/frontend/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/yarn.lock b/yarn.lock
index 1b3baac..58e4766 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -691,6 +691,56 @@ __metadata:
languageName: node
linkType: hard
+"@aws-sdk/client-ecr@npm:^3.568.0":
+ version: 3.699.0
+ resolution: "@aws-sdk/client-ecr@npm:3.699.0"
+ dependencies:
+ "@aws-crypto/sha256-browser": "npm:5.2.0"
+ "@aws-crypto/sha256-js": "npm:5.2.0"
+ "@aws-sdk/client-sso-oidc": "npm:3.699.0"
+ "@aws-sdk/client-sts": "npm:3.699.0"
+ "@aws-sdk/core": "npm:3.696.0"
+ "@aws-sdk/credential-provider-node": "npm:3.699.0"
+ "@aws-sdk/middleware-host-header": "npm:3.696.0"
+ "@aws-sdk/middleware-logger": "npm:3.696.0"
+ "@aws-sdk/middleware-recursion-detection": "npm:3.696.0"
+ "@aws-sdk/middleware-user-agent": "npm:3.696.0"
+ "@aws-sdk/region-config-resolver": "npm:3.696.0"
+ "@aws-sdk/types": "npm:3.696.0"
+ "@aws-sdk/util-endpoints": "npm:3.696.0"
+ "@aws-sdk/util-user-agent-browser": "npm:3.696.0"
+ "@aws-sdk/util-user-agent-node": "npm:3.696.0"
+ "@smithy/config-resolver": "npm:^3.0.12"
+ "@smithy/core": "npm:^2.5.3"
+ "@smithy/fetch-http-handler": "npm:^4.1.1"
+ "@smithy/hash-node": "npm:^3.0.10"
+ "@smithy/invalid-dependency": "npm:^3.0.10"
+ "@smithy/middleware-content-length": "npm:^3.0.12"
+ "@smithy/middleware-endpoint": "npm:^3.2.3"
+ "@smithy/middleware-retry": "npm:^3.0.27"
+ "@smithy/middleware-serde": "npm:^3.0.10"
+ "@smithy/middleware-stack": "npm:^3.0.10"
+ "@smithy/node-config-provider": "npm:^3.1.11"
+ "@smithy/node-http-handler": "npm:^3.3.1"
+ "@smithy/protocol-http": "npm:^4.1.7"
+ "@smithy/smithy-client": "npm:^3.4.4"
+ "@smithy/types": "npm:^3.7.1"
+ "@smithy/url-parser": "npm:^3.0.10"
+ "@smithy/util-base64": "npm:^3.0.0"
+ "@smithy/util-body-length-browser": "npm:^3.0.0"
+ "@smithy/util-body-length-node": "npm:^3.0.0"
+ "@smithy/util-defaults-mode-browser": "npm:^3.0.27"
+ "@smithy/util-defaults-mode-node": "npm:^3.0.27"
+ "@smithy/util-endpoints": "npm:^2.1.6"
+ "@smithy/util-middleware": "npm:^3.0.10"
+ "@smithy/util-retry": "npm:^3.0.10"
+ "@smithy/util-utf8": "npm:^3.0.0"
+ "@smithy/util-waiter": "npm:^3.1.9"
+ tslib: "npm:^2.6.2"
+ checksum: 10c0/2baa287019d591231528c870a144ab76b53efe408285e03509c80a3910692a3a360922de65a8988dbcac57e55fbd4462bf127eab4e0d8cfda917c42702dfa8a8
+ languageName: node
+ linkType: hard
+
"@aws-sdk/client-ecs@npm:^3.511.0":
version: 3.699.0
resolution: "@aws-sdk/client-ecs@npm:3.699.0"
@@ -2424,6 +2474,85 @@ __metadata:
languageName: unknown
linkType: soft
+"@aws/ecr-plugin-for-backstage-backend@workspace:^, @aws/ecr-plugin-for-backstage-backend@workspace:plugins/ecr/backend":
+ version: 0.0.0-use.local
+ resolution: "@aws/ecr-plugin-for-backstage-backend@workspace:plugins/ecr/backend"
+ dependencies:
+ "@aws-sdk/client-ecr": "npm:^3.568.0"
+ "@aws-sdk/middleware-sdk-sts": "npm:^3.523.0"
+ "@aws-sdk/types": "npm:^3.609.0"
+ "@aws-sdk/util-arn-parser": "npm:^3.495.0"
+ "@aws/aws-core-plugin-for-backstage-common": "workspace:^"
+ "@backstage/backend-common": "npm:^0.25.0"
+ "@backstage/backend-defaults": "npm:^0.5.3"
+ "@backstage/backend-plugin-api": "npm:^1.0.2"
+ "@backstage/backend-test-utils": "npm:^1.1.0"
+ "@backstage/catalog-client": "npm:^1.8.0"
+ "@backstage/catalog-model": "npm:^1.7.1"
+ "@backstage/cli": "npm:^0.29.2"
+ "@backstage/config": "npm:^1.3.0"
+ "@backstage/errors": "npm:^1.2.5"
+ "@backstage/integration-aws-node": "npm:^0.1.13"
+ "@backstage/plugin-auth-backend": "npm:^0.24.0"
+ "@backstage/plugin-auth-backend-module-guest-provider": "npm:^0.2.2"
+ "@backstage/plugin-catalog-node": "npm:^1.14.0"
+ "@types/express": "npm:*"
+ "@types/luxon": "npm:^3"
+ "@types/regression": "npm:^2"
+ "@types/supertest": "npm:^2.0.12"
+ aws-sdk-client-mock: "npm:^4.0.0"
+ express: "npm:^4.17.1"
+ express-promise-router: "npm:^4.1.0"
+ luxon: "npm:^3.4.4"
+ msw: "npm:^1.0.0"
+ node-fetch: "npm:^2.6.7"
+ regression: "npm:^2.0.1"
+ supertest: "npm:^6.2.4"
+ winston: "npm:^3.2.1"
+ yn: "npm:^4.0.0"
+ languageName: unknown
+ linkType: soft
+
+"@aws/ecr-plugin-for-backstage@workspace:^, @aws/ecr-plugin-for-backstage@workspace:plugins/ecr/frontend":
+ version: 0.0.0-use.local
+ resolution: "@aws/ecr-plugin-for-backstage@workspace:plugins/ecr/frontend"
+ dependencies:
+ "@aws-sdk/client-ecr": "npm:^3.568.0"
+ "@aws-sdk/util-arn-parser": "npm:^3.495.0"
+ "@aws/aws-core-plugin-for-backstage-common": "workspace:^"
+ "@aws/aws-core-plugin-for-backstage-react": "workspace:^"
+ "@aws/ecr-plugin-for-backstage-backend": "workspace:^"
+ "@backstage/catalog-model": "npm:^1.7.1"
+ "@backstage/cli": "npm:^0.29.2"
+ "@backstage/core-app-api": "npm:^1.15.2"
+ "@backstage/core-components": "npm:^0.16.1"
+ "@backstage/core-plugin-api": "npm:^1.10.1"
+ "@backstage/dev-utils": "npm:^1.1.4"
+ "@backstage/errors": "npm:^1.2.5"
+ "@backstage/plugin-catalog-react": "npm:^1.14.2"
+ "@backstage/test-utils": "npm:^1.7.2"
+ "@backstage/theme": "npm:^0.6.2"
+ "@material-ui/core": "npm:^4.12.2"
+ "@material-ui/icons": "npm:^4.9.1"
+ "@material-ui/lab": "npm:4.0.0-alpha.61"
+ "@testing-library/dom": "npm:^9.0.0"
+ "@testing-library/jest-dom": "npm:^6.0.0"
+ "@testing-library/react": "npm:^14.0.0"
+ "@testing-library/user-event": "npm:^14.0.0"
+ "@types/dateformat": "npm:^5"
+ "@types/humanize-duration": "npm:^3"
+ dateformat: "npm:^5.0.3"
+ humanize-duration: "npm:^3.31.0"
+ msw: "npm:^1.0.0"
+ react-use: "npm:^17.2.4"
+ recharts: "npm:^2.12.7"
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
+ react-router-dom: 6.0.0-beta.0 || ^6.3.0
+ languageName: unknown
+ linkType: soft
+
"@azure/abort-controller@npm:^1.0.0":
version: 1.1.0
resolution: "@azure/abort-controller@npm:1.1.0"
@@ -16559,6 +16688,7 @@ __metadata:
"@aws/aws-codebuild-plugin-for-backstage": "workspace:^"
"@aws/aws-codepipeline-plugin-for-backstage": "workspace:^"
"@aws/cost-insights-plugin-for-backstage": "workspace:^"
+ "@aws/ecr-plugin-for-backstage": "workspace:^"
"@backstage-community/plugin-cost-insights": "npm:^0.12.25"
"@backstage-community/plugin-github-actions": "npm:^0.6.16"
"@backstage-community/plugin-tech-radar": "npm:^0.7.4"
@@ -17392,6 +17522,7 @@ __metadata:
"@aws/aws-codepipeline-plugin-for-backstage-backend": "workspace:^"
"@aws/aws-core-plugin-for-backstage-scaffolder-actions": "workspace:^"
"@aws/cost-insights-plugin-for-backstage-backend": "workspace:^"
+ "@aws/ecr-plugin-for-backstage-backend": "workspace:^"
"@backstage/backend-defaults": "npm:^0.5.3"
"@backstage/backend-plugin-api": "npm:^1.0.2"
"@backstage/catalog-client": "npm:^1.8.0"