Repository: openshift/console
Pull request #14115: OCPBUGS-36652: Remove the Backend Common Proxy and Replace it with Dedicated ones
Author: login: Lucifergene
Labels: name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: tide/merge-method-squash
description: Denotes a PR that should be squashed by tide when it merges.
name: component/shared
description: Related to console-shared
name: component/dev-console
description: Related to dev-console
name: component/backend
description: Related to backend
name: component/git-service
description: Related to git-service
name: component/pipelines
description: Related to pipelines-plugin
name: jira/valid-bug
description: Indicates that a referenced Jira bug is valid for the branch this PR is targeting.
name: jira/valid-reference
description: Indicates that this PR references a valid Jira ticket of any type.
Created: 2024-08-07T14:30:27Z | Merged: 2025-03-14T13:17:20Z

Jira Task: https://issues.redhat.com/browse/ODC-7764

Removed the common devconsole proxy.

Created 6 new endpoints:

1. Artifacthub

- POST /api/dev-console/artifacthub/search
- POST /api/dev-console/artifacthub/get
- POST /api/dev-console/artifacthub/yaml

2. Tekton-results

- POST /api/dev-console/tekton-results/get
- POST /api/dev-console/tekton-results/logs
- POST /api/dev-console/tekton-results/summary

3. Webhooks

- POST /api/dev-console/webhooks/github
- POST /api/dev-console/webhooks/gitlab
- POST /api/dev-console/webhooks/bitbucket


Diff:
diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go
index c7467a807f2..86383ea11f9 100644
--- a/cmd/bridge/main.go
+++ b/cmd/bridge/main.go
@@ -316,6 +316,9 @@ func main() {
 		srv.GOOS = runtime.GOOS
 	}
 
+	// Blacklisted headers
+	srv.ProxyHeaderDenyList = []string{"Cookie", "X-CSRFToken", "X-CSRF-Token"}
+
 	if *fLogLevel != "" {
 		klog.Warningf("DEPRECATED: --log-level is now deprecated, use verbosity flag --v=Level instead")
 	}
@@ -352,7 +355,7 @@ func main() {
 
 		srv.K8sProxyConfig = &proxy.Config{
 			TLSClientConfig: tlsConfig,
-			HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+			HeaderBlacklist: srv.ProxyHeaderDenyList,
 			Endpoint:        k8sEndpoint,
 		}
 
@@ -383,33 +386,33 @@ func main() {
 
 			srv.ThanosProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: openshiftThanosHost, Path: "/api"},
 			}
 			srv.ThanosTenancyProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: openshiftThanosTenancyHost, Path: "/api"},
 			}
 			srv.ThanosTenancyProxyForRulesConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: openshiftThanosTenancyForRulesHost, Path: "/api"},
 			}
 
 			srv.AlertManagerProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: openshiftAlertManagerHost, Path: "/api"},
 			}
 			srv.AlertManagerUserWorkloadProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: *fAlertmanagerUserWorkloadHost, Path: "/api"},
 			}
 			srv.AlertManagerTenancyProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: *fAlertmanagerTenancyHost, Path: "/api"},
 			}
 			srv.TerminalProxyTLSConfig = serviceProxyTLSConfig
@@ -417,7 +420,7 @@ func main() {
 
 			srv.GitOpsProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        &url.URL{Scheme: "https", Host: openshiftGitOpsHost},
 			}
 		}
@@ -443,7 +446,7 @@ func main() {
 
 		srv.K8sProxyConfig = &proxy.Config{
 			TLSClientConfig:         serviceProxyTLSConfig,
-			HeaderBlacklist:         []string{"Cookie", "X-CSRFToken"},
+			HeaderBlacklist:         srv.ProxyHeaderDenyList,
 			Endpoint:                k8sEndpoint,
 			UseProxyFromEnvironment: true,
 		}
@@ -464,17 +467,17 @@ func main() {
 			offClusterThanosURL.Path += "/api"
 			srv.ThanosTenancyProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterThanosURL,
 			}
 			srv.ThanosTenancyProxyForRulesConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterThanosURL,
 			}
 			srv.ThanosProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterThanosURL,
 			}
 		}
@@ -486,17 +489,17 @@ func main() {
 			offClusterAlertManagerURL.Path += "/api"
 			srv.AlertManagerProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterAlertManagerURL,
 			}
 			srv.AlertManagerTenancyProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterAlertManagerURL,
 			}
 			srv.AlertManagerUserWorkloadProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterAlertManagerURL,
 			}
 		}
@@ -510,7 +513,7 @@ func main() {
 
 			srv.GitOpsProxyConfig = &proxy.Config{
 				TLSClientConfig: serviceProxyTLSConfig,
-				HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+				HeaderBlacklist: srv.ProxyHeaderDenyList,
 				Endpoint:        offClusterGitOpsURL,
 			}
 		}
@@ -530,7 +533,7 @@ func main() {
 	}
 	srv.ClusterManagementProxyConfig = &proxy.Config{
 		TLSClientConfig: oscrypto.SecureTLSConfig(&tls.Config{}),
-		HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
+		HeaderBlacklist: srv.ProxyHeaderDenyList,
 		Endpoint:        clusterManagementURL,
 	}
 
diff --git a/frontend/packages/console-shared/src/types/backend-api.ts b/frontend/packages/console-shared/src/types/backend-api.ts
new file mode 100644
index 00000000000..cc8e049ca9f
--- /dev/null
+++ b/frontend/packages/console-shared/src/types/backend-api.ts
@@ -0,0 +1,5 @@
+export type DevConsoleEndpointResponse = {
+  statusCode: number;
+  headers: Record<string, string[]>;
+  body: string;
+};
diff --git a/frontend/packages/console-shared/src/types/index.ts b/frontend/packages/console-shared/src/types/index.ts
index c9ac0a64357..b205e7b3291 100644
--- a/frontend/packages/console-shared/src/types/index.ts
+++ b/frontend/packages/console-shared/src/types/index.ts
@@ -2,3 +2,4 @@ export * from './pod';
 export * from './resource';
 export * from './route-params';
 export * from './tableColumn';
+export * from './backend-api';
diff --git a/frontend/packages/console-shared/src/utils/proxy.ts b/frontend/packages/console-shared/src/utils/proxy.ts
deleted file mode 100644
index 8cf8bd96e7f..00000000000
--- a/frontend/packages/console-shared/src/utils/proxy.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { safeLoad } from 'js-yaml';
-import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
-import { HttpError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error';
-
-export const API_PROXY_URL = '/api/dev-console/proxy/internet';
-
-type ProxyRequest = {
-  allowInsecure?: boolean;
-  allowAuthHeader?: boolean;
-  method: string;
-  url: string;
-  headers?: Record<string, string[]>;
-  queryparams?: Record<string, string[]>;
-  body?: string;
-};
-
-export type ProxyResponse = {
-  statusCode: number;
-  headers: Record<string, string[]>;
-  body: string;
-};
-
-const isJSONString = (str: string): boolean => {
-  try {
-    JSON.parse(str);
-  } catch (e) {
-    return false;
-  }
-  return true;
-};
-
-export const convertHeaders = (headers): Record<string, string[]> => {
-  return Object.keys(headers).reduce((output, key) => {
-    output[key] = [headers[key]];
-    return output;
-  }, {});
-};
-
-/**
- * Calls the proxy in our backend to bypass CORS headers.
- */
-export const consoleProxyFetch = async (proxyRequest: ProxyRequest): Promise<ProxyResponse> => {
-  const proxyResponse: ProxyResponse = await consoleFetchJSON.post(API_PROXY_URL, proxyRequest);
-  if (!proxyResponse.statusCode) {
-    throw new Error('Unexpected proxy response: Status code is missing!');
-  }
-  if (proxyResponse.statusCode < 200 || proxyResponse.statusCode >= 300) {
-    throw new HttpError(
-      `Unexpected status code: ${proxyResponse.statusCode}`,
-      proxyResponse.statusCode,
-      null,
-      proxyResponse,
-    );
-  }
-  return proxyResponse;
-};
-
-export const consoleProxyFetchJSON = <T>(proxyRequest: ProxyRequest): Promise<T> => {
-  return consoleProxyFetch(proxyRequest).then((response) => {
-    return isJSONString(response.body) ? JSON.parse(response.body) : safeLoad(response.body);
-  });
-};
diff --git a/frontend/packages/git-service/src/services/bitbucket-service.ts b/frontend/packages/git-service/src/services/bitbucket-service.ts
index 34ee54dcf9c..2527c655374 100644
--- a/frontend/packages/git-service/src/services/bitbucket-service.ts
+++ b/frontend/packages/git-service/src/services/bitbucket-service.ts
@@ -2,12 +2,7 @@ import { Base64 } from 'js-base64';
 import * as ParseBitbucketUrl from 'parse-bitbucket-url';
 import 'whatwg-fetch';
 import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
-import {
-  API_PROXY_URL,
-  ProxyResponse,
-  consoleProxyFetchJSON,
-  convertHeaders,
-} from '@console/shared/src/utils/proxy';
+import { DevConsoleEndpointResponse } from '@console/shared/src';
 import {
   GitSource,
   SecretType,
@@ -19,6 +14,24 @@ import {
 } from '../types';
 import { BaseService } from './base-service';
 
+type BBWebhookBody = {
+  url: string;
+  events: string[];
+  skip_cert_verification: boolean;
+  active: boolean;
+};
+
+type BitbucketWebhookRequest = {
+  headers: Headers;
+  isServer: boolean;
+  baseURL: string;
+  owner: string;
+  repoName: string;
+  body: BBWebhookBody;
+};
+
+export const BITBUCKET_WEBHOOK_BACKEND_URL = '/api/dev-console/webhooks/bitbucket';
+
 export class BitbucketService extends BaseService {
   private readonly metadata: RepoMetadata;
 
@@ -61,15 +74,6 @@ export class BitbucketService extends BaseService {
       ...headers,
     };
 
-    if (this.isServer) {
-      return consoleProxyFetchJSON({
-        url,
-        method: requestMethod || 'GET',
-        headers: convertHeaders(requestHeaders),
-        ...(body && { body: JSON.stringify(body) }),
-      });
-    }
-
     const response = await fetch(url, {
       method: requestMethod || 'GET',
       headers: requestHeaders,
@@ -183,28 +187,33 @@ export class BitbucketService extends BaseService {
     webhookURL: string,
     sslVerification: boolean,
   ): Promise<boolean> => {
-    const headers = {
-      'Content-Type': ['application/json'],
-      Authorization: [`Basic ${token}`],
-    };
-    const body = {
+    const headers = new Headers({
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${token}`,
+    });
+    const body: BBWebhookBody = {
       url: webhookURL,
       events: ['repo:push', 'pullrequest:created', 'pullrequest:updated'],
       skip_cert_verification: !sslVerification,
       active: true,
     };
-    const url = this.isServer
-      ? `${this.baseURL}/projects/${this.metadata.owner}/repos/${this.metadata.repoName}/hooks`
-      : `${this.baseURL}/repositories/${this.metadata.owner}/${this.metadata.repoName}/hooks`;
 
-    /* Using DevConsole Proxy to create webhook as Bitbucket is giving CORS error */
-    const webhookResponse: ProxyResponse = await consoleFetchJSON.post(API_PROXY_URL, {
-      url,
-      method: 'POST',
+    const webhookRequestBody: BitbucketWebhookRequest = {
       headers,
-      body: JSON.stringify(body),
-    });
+      isServer: this.isServer,
+      baseURL: this.baseURL,
+      owner: this.metadata.owner,
+      repoName: this.metadata.repoName,
+      body,
+    };
 
+    const webhookResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+      BITBUCKET_WEBHOOK_BACKEND_URL,
+      webhookRequestBody,
+    );
+    if (!webhookResponse.statusCode) {
+      throw new Error('Unexpected proxy response: Status code is missing!');
+    }
     return webhookResponse.statusCode === 201;
   };
 
diff --git a/frontend/packages/git-service/src/services/gitea-service.ts b/frontend/packages/git-service/src/services/gitea-service.ts
index df27df37b77..84b2f821574 100644
--- a/frontend/packages/git-service/src/services/gitea-service.ts
+++ b/frontend/packages/git-service/src/services/gitea-service.ts
@@ -3,12 +3,6 @@ import { Base64 } from 'js-base64';
 import * as _ from 'lodash';
 import 'whatwg-fetch';
 import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
-import {
-  API_PROXY_URL,
-  ProxyResponse,
-  consoleProxyFetchJSON,
-  convertHeaders,
-} from '@console/shared/src/utils/proxy';
 import {
   BranchList,
   GitSource,
@@ -24,14 +18,9 @@ import { BaseService } from './base-service';
 export class GiteaService extends BaseService {
   private readonly metadata: RepoMetadata;
 
-  private isServer = false;
-
   constructor(gitsource: GitSource) {
     super(gitsource);
     this.metadata = this.getRepoMetadata();
-    if (!this.metadata.host.includes('gitea.com')) {
-      this.isServer = true;
-    }
   }
 
   protected getAuthProvider = (): any => {
@@ -58,15 +47,6 @@ export class GiteaService extends BaseService {
       ...authHeaders,
       ...headers,
     };
-    if (this.isServer) {
-      return consoleProxyFetchJSON({
-        allowInsecure: true,
-        url,
-        method: requestMethod || 'GET',
-        headers: convertHeaders(requestHeaders),
-        ...(body && { body: JSON.stringify(body) }),
-      });
-    }
 
     const response = await fetch(url, {
       method: requestMethod || 'GET',
@@ -149,10 +129,10 @@ export class GiteaService extends BaseService {
 
   /* TODO: Gitea PAC Support */
   createRepoWebhook = async (token: string, webhookURL: string): Promise<boolean> => {
-    const headers = {
-      'Content-Type': ['application/json'],
-      Authorization: [`token ${token}`],
-    };
+    const headers = new Headers({
+      'Content-Type': 'application/json',
+      Authorization: `token ${token}`,
+    });
     const body = {
       active: true,
       authorization_header: '',
@@ -166,15 +146,9 @@ export class GiteaService extends BaseService {
     };
     const url = `${this.metadata.host}/api/v1/repos/${this.metadata.owner}/${this.metadata.repoName}/hooks`;
 
-    /* Using DevConsole Proxy to create webhook as Gitea is giving CORS error */
-    const webhookResponse: ProxyResponse = await consoleFetchJSON.post(API_PROXY_URL, {
-      url,
-      method: 'POST',
-      headers,
-      body: JSON.stringify(body),
-    });
+    const webhookResponse: Response = await consoleFetchJSON.post(url, body, { headers });
 
-    return webhookResponse.statusCode === 201;
+    return webhookResponse.status === 201;
   };
 
   isFilePresent = async (path: string): Promise<boolean> => {
diff --git a/frontend/packages/git-service/src/services/github-service.ts b/frontend/packages/git-service/src/services/github-service.ts
index 14336579f44..d59deb61f91 100644
--- a/frontend/packages/git-service/src/services/github-service.ts
+++ b/frontend/packages/git-service/src/services/github-service.ts
@@ -2,7 +2,7 @@ import * as Octokit from '@octokit/rest';
 import * as GitUrlParse from 'git-url-parse';
 import { Base64 } from 'js-base64';
 import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
-import { API_PROXY_URL, ProxyResponse } from '@console/shared/src/utils/proxy';
+import { DevConsoleEndpointResponse } from '@console/shared/src';
 import {
   GitSource,
   SecretType,
@@ -14,6 +14,27 @@ import {
 } from '../types';
 import { BaseService } from './base-service';
 
+type GHWebhookBody = {
+  name: string;
+  active: boolean;
+  config: {
+    url: string;
+    content_type: string;
+    insecure_ssl: string;
+    secret: string;
+  };
+  events: string[];
+};
+
+type GithubWebhookRequest = {
+  headers: Headers;
+  hostName: string;
+  owner: string;
+  repoName: string;
+  body: GHWebhookBody;
+};
+
+export const GITHUB_WEBHOOK_BACKEND_URL = '/api/dev-console/webhooks/github';
 export class GithubService extends BaseService {
   private readonly client: Octokit;
 
@@ -138,12 +159,12 @@ export class GithubService extends BaseService {
     sslVerification: boolean,
     webhookSecret: string,
   ): Promise<boolean> => {
-    const headers = {
-      Accept: ['application/vnd.github+json'],
-      Authorization: [`Bearer ${token}`],
-      'X-GitHub-Api-Version': ['2022-11-28'],
-    };
-    const body = {
+    const headers = new Headers({
+      Accept: 'application/vnd.github+json',
+      Authorization: `Bearer ${token}`,
+      'X-GitHub-Api-Version': '2022-11-28',
+    });
+    const body: GHWebhookBody = {
       name: 'web',
       active: true,
       config: {
@@ -158,13 +179,22 @@ export class GithubService extends BaseService {
       this.metadata.host === 'github.com'
         ? `https://api.github.com`
         : `https://${this.metadata.host}/api/v3`;
-    /* Using DevConsole Proxy to create webhook as Octokit is giving CORS error */
-    const webhookResponse: ProxyResponse = await consoleFetchJSON.post(API_PROXY_URL, {
-      url: `${AddWebhookBaseURL}/repos/${this.metadata.owner}/${this.metadata.repoName}/hooks`,
-      method: 'POST',
+
+    const webhookRequestBody: GithubWebhookRequest = {
       headers,
-      body: JSON.stringify(body),
-    });
+      hostName: AddWebhookBaseURL,
+      owner: this.metadata.owner,
+      repoName: this.metadata.repoName,
+      body,
+    };
+
+    const webhookResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+      GITHUB_WEBHOOK_BACKEND_URL,
+      webhookRequestBody,
+    );
+    if (!webhookResponse.statusCode) {
+      throw new Error('Unexpected proxy response: Status code is missing!');
+    }
 
     return webhookResponse.statusCode === 201;
   };
diff --git a/frontend/packages/git-service/src/services/gitlab-service.ts b/frontend/packages/git-service/src/services/gitlab-service.ts
index 1654c903ccd..26acf44c63c 100644
--- a/frontend/packages/git-service/src/services/gitlab-service.ts
+++ b/frontend/packages/git-service/src/services/gitlab-service.ts
@@ -3,7 +3,7 @@ import { Gitlab } from 'gitlab';
 import i18n from 'i18next';
 import { Base64 } from 'js-base64';
 import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
-import { API_PROXY_URL, ProxyResponse } from '@console/shared/src/utils/proxy';
+import { DevConsoleEndpointResponse } from '@console/shared/src';
 import {
   GitSource,
   SecretType,
@@ -20,6 +20,23 @@ type GitlabRepo = {
   path_with_namespace: string;
 };
 
+type GLWebhookBody = {
+  url: string;
+  push_events: boolean;
+  merge_requests_events: boolean;
+  enable_ssl_verification: boolean;
+  token: string;
+};
+
+type GitlabWebhookRequest = {
+  headers: Headers;
+  hostName: string;
+  projectID: string;
+  body: GLWebhookBody;
+};
+
+export const GITLAB_WEBHOOK_BACKEND_URL = '/api/dev-console/webhooks/gitlab';
+
 const removeLeadingSlash = (str: string) => str?.replace(/^\//, '') || '';
 
 export class GitlabService extends BaseService {
@@ -171,25 +188,32 @@ export class GitlabService extends BaseService {
     webhookSecret: string,
   ): Promise<boolean> => {
     const projectID = await this.getProjectId();
-    const headers = {
-      'Content-Type': ['application/json'],
-      'PRIVATE-TOKEN': [token],
-    };
-    const body = {
+    const headers = new Headers({
+      'Content-Type': 'application/json',
+      'PRIVATE-TOKEN': token,
+    });
+    const body: GLWebhookBody = {
       url: webhookURL,
       push_events: true,
       merge_requests_events: true,
       enable_ssl_verification: sslVerification,
       token: webhookSecret,
     };
-    /* Using DevConsole Proxy to create webhook as Gitlab is giving CORS error */
-    const webhookResponse: ProxyResponse = await consoleFetchJSON.post(API_PROXY_URL, {
-      url: `${this.metadata.host}/api/v4/projects/${projectID}/hooks`,
-      method: 'POST',
+
+    const webhookRequestBody: GitlabWebhookRequest = {
       headers,
-      body: JSON.stringify(body),
-    });
+      hostName: this.metadata.host,
+      projectID: projectID.toString(),
+      body,
+    };
 
+    const webhookResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+      GITLAB_WEBHOOK_BACKEND_URL,
+      webhookRequestBody,
+    );
+    if (!webhookResponse.statusCode) {
+      throw new Error('Unexpected proxy response: Status code is missing!');
+    }
     return webhookResponse.statusCode === 201;
   };
 
diff --git a/frontend/packages/pipelines-plugin/src/components/catalog/apis/artifactHub.ts b/frontend/packages/pipelines-plugin/src/components/catalog/apis/artifactHub.ts
index 5fbb723b31a..2b1b0971596 100644
--- a/frontend/packages/pipelines-plugin/src/components/catalog/apis/artifactHub.ts
+++ b/frontend/packages/pipelines-plugin/src/components/catalog/apis/artifactHub.ts
@@ -2,59 +2,23 @@ import * as React from 'react';
 import * as _ from 'lodash';
 import { CatalogItem } from '@console/dynamic-plugin-sdk';
 import { K8sResourceKind, k8sCreate, k8sUpdate } from '@console/internal/module/k8s';
-import { consoleProxyFetchJSON } from '@console/shared/src/utils/proxy';
-import { ARTIFACTHUB_API_BASE_URL } from '../../../const';
+import { GITHUB_BASE_URL } from '../../../const';
 import { TaskModel, TaskModelV1Beta1 } from '../../../models';
 import { TektonTaskAnnotation } from '../../pipelines/const';
 import { ARTIFACTHUB } from '../../quicksearch/const';
 import { ApiResult } from '../hooks/useApiResponse';
-
-export type ArtifactHubRepository = {
-  name: string;
-  kind: number;
-  url: string;
-  display_name: string;
-  repository_id: string;
-  organization_name: string;
-  organization_display_name: string;
-};
-
-export type ArtifactHubVersion = {
-  version: string;
-  contains_security_update: boolean;
-  prerelease: boolean;
-  ts: number;
-};
-
-export type ArtifactHubTask = {
-  package_id: string;
-  name: string;
-  description: string;
-  version: string;
-  display_name: string;
-  repository: ArtifactHubRepository;
-};
-
-export type ArtifactHubTaskDetails = {
-  package_id: string;
-  name: string;
-  description: string;
-  display_name: string;
-  keywords: string[];
-  platforms: string[];
-  version: ArtifactHubVersion[];
-  available_versions: [];
-  content_url: string;
-  repository: ArtifactHubRepository;
-};
-
-const ARTIFACRHUB_TASKS_SEARCH_URL = `${ARTIFACTHUB_API_BASE_URL}/packages/search?offset=0&limit=60&facets=false&kind=7&deprecated=false&sort=relevance`;
+import {
+  ArtifactHubTask,
+  ArtifactHubTaskDetails,
+  getTaskDetails,
+  getTaskYAMLFromGithub,
+  searchTasks,
+} from './utils';
 
 export const getArtifactHubTaskDetails = async (
   item: CatalogItem,
   v?: string,
 ): Promise<ArtifactHubTaskDetails> => {
-  const API_BASE_URL = `${ARTIFACTHUB_API_BASE_URL}/packages/tekton-task`;
   const { name, data } = item;
   const {
     task: {
@@ -62,8 +26,8 @@ export const getArtifactHubTaskDetails = async (
       repository: { name: repoName },
     },
   } = data;
-  const url = `${API_BASE_URL}/${repoName}/${name}/${v || version}`;
-  return consoleProxyFetchJSON({ url, method: 'GET' });
+
+  return getTaskDetails({ repoName, name, version: v || version });
 };
 
 export const useGetArtifactHubTasks = (hasPermission: boolean): ApiResult<ArtifactHubTask[]> => {
@@ -74,11 +38,8 @@ export const useGetArtifactHubTasks = (hasPermission: boolean): ApiResult<Artifa
   React.useEffect(() => {
     let mounted = true;
     if (hasPermission) {
-      consoleProxyFetchJSON<{ packages: ArtifactHubTask[] }>({
-        url: ARTIFACRHUB_TASKS_SEARCH_URL,
-        method: 'GET',
-      })
-        .then(({ packages }) => {
+      searchTasks()
+        .then((packages) => {
           if (mounted) {
             setLoaded(true);
             setResult(packages);
@@ -101,7 +62,12 @@ export const useGetArtifactHubTasks = (hasPermission: boolean): ApiResult<Artifa
 };
 
 export const createArtifactHubTask = (url: string, namespace: string, version: string) => {
-  return consoleProxyFetchJSON({ url, method: 'GET' })
+  const yamlPath = url.startsWith(GITHUB_BASE_URL) ? url.slice(GITHUB_BASE_URL.length) : null;
+  if (!yamlPath) {
+    throw new Error('Not a valid GitHub URL');
+  }
+
+  return getTaskYAMLFromGithub({ yamlPath })
     .then((task: K8sResourceKind) => {
       task.metadata.namespace = namespace;
       task.metadata.annotations = {
@@ -125,7 +91,12 @@ export const updateArtifactHubTask = async (
   name: string,
   version: string,
 ) => {
-  return consoleProxyFetchJSON({ url, method: 'GET' })
+  const yamlPath = url.startsWith(GITHUB_BASE_URL) ? url.slice(GITHUB_BASE_URL.length) : null;
+  if (!yamlPath) {
+    throw new Error('Not a valid GitHub raw URL');
+  }
+
+  return getTaskYAMLFromGithub({ yamlPath })
     .then((task: K8sResourceKind) => {
       task.metadata.namespace = namespace;
       task.metadata.annotations = {
diff --git a/frontend/packages/pipelines-plugin/src/components/catalog/apis/utils.ts b/frontend/packages/pipelines-plugin/src/components/catalog/apis/utils.ts
new file mode 100644
index 00000000000..948a7552a2d
--- /dev/null
+++ b/frontend/packages/pipelines-plugin/src/components/catalog/apis/utils.ts
@@ -0,0 +1,162 @@
+import { safeLoad } from 'js-yaml';
+import { consoleFetchJSON, K8sResourceKind } from '@console/dynamic-plugin-sdk/src/lib-core';
+import { HttpError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error';
+
+export const ARTIFACTHUB_SEARCH_URL = '/api/dev-console/artifacthub/search';
+export const ARTIFACTHUB_TASK_DETAILS_URL = '/api/dev-console/artifacthub/get';
+export const GITHUB_ARTIFACTHUB_TASK_YAML_URL = '/api/dev-console/artifacthub/yaml';
+
+export type ArtifactHubRepository = {
+  name: string;
+  kind: number;
+  url: string;
+  display_name: string;
+  repository_id: string;
+  organization_name: string;
+  organization_display_name: string;
+};
+
+export type ArtifactHubVersion = {
+  version: string;
+  contains_security_update: boolean;
+  prerelease: boolean;
+  ts: number;
+};
+
+export type ArtifactHubTask = {
+  package_id: string;
+  name: string;
+  description: string;
+  version: string;
+  display_name: string;
+  repository: ArtifactHubRepository;
+};
+
+export type ArtifactHubTaskDetails = {
+  package_id: string;
+  name: string;
+  description: string;
+  display_name: string;
+  keywords: string[];
+  platforms: string[];
+  version: ArtifactHubVersion[];
+  available_versions: [];
+  content_url: string;
+  repository: ArtifactHubRepository;
+};
+
+export type DevConsoleEndpointResponse = {
+  statusCode: number;
+  headers: Record<string, string[]>;
+  body: string;
+};
+
+type TaskSearchRequest = {
+  searchQuery?: string;
+};
+
+type TaskDetailsRequest = {
+  repoName: string;
+  name: string;
+  version: string;
+};
+
+type TaskYAMLRequest = {
+  yamlPath: string;
+};
+
+/**
+ * Fetches the YAML content of a task from GitHub.
+ * @param taskYAMLRequest The request object containing the path to the task YAML file.
+ * @returns The parsed YAML content of the task.
+ */
+export const getTaskYAMLFromGithub = async (
+  taskYAMLRequest: TaskYAMLRequest,
+): Promise<K8sResourceKind> => {
+  const taskYAMLResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+    GITHUB_ARTIFACTHUB_TASK_YAML_URL,
+    taskYAMLRequest,
+  );
+
+  if (!taskYAMLResponse.statusCode) {
+    throw new Error('Unexpected proxy response: Status code is missing!');
+  }
+
+  if (taskYAMLResponse.statusCode < 200 || taskYAMLResponse.statusCode >= 300) {
+    throw new HttpError(
+      `Unexpected status code: ${taskYAMLResponse.statusCode}`,
+      taskYAMLResponse.statusCode,
+      null,
+      taskYAMLResponse,
+    );
+  }
+
+  try {
+    // Parse the YAML response body
+    return safeLoad(taskYAMLResponse.body);
+  } catch (e) {
+    throw new Error('Failed to parse task YAML response body as YAML');
+  }
+};
+
+/**
+ * Fetches the details of a task from ArtifactHub.
+ * @param taskDetailsRequest The request object containing the task details.
+ * @returns The details of the task.
+ */
+export const getTaskDetails = async (
+  taskDetailsRequest: TaskDetailsRequest,
+): Promise<ArtifactHubTaskDetails> => {
+  const taskDetailsResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+    ARTIFACTHUB_TASK_DETAILS_URL,
+    taskDetailsRequest,
+  );
+  if (!taskDetailsResponse.statusCode) {
+    throw new Error('Unexpected proxy response: Status code is missing!');
+  }
+  if (taskDetailsResponse.statusCode < 200 || taskDetailsResponse.statusCode >= 300) {
+    throw new HttpError(
+      `Unexpected status code: ${taskDetailsResponse.statusCode}`,
+      taskDetailsResponse.statusCode,
+      null,
+      taskDetailsResponse,
+    );
+  }
+
+  try {
+    return JSON.parse(taskDetailsResponse.body) as ArtifactHubTaskDetails;
+  } catch (e) {
+    throw new Error('Failed to parse task details response body as JSON');
+  }
+};
+
+/**
+ * Fetches the tasks from ArtifactHub.
+ * @param (optional) searchrequest The search request object.
+ * @returns The array of tasks matching the search request.
+ */
+export const searchTasks = async (
+  searchrequest?: TaskSearchRequest,
+): Promise<ArtifactHubTask[]> => {
+  const searchResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+    ARTIFACTHUB_SEARCH_URL,
+    searchrequest || {},
+  );
+  if (!searchResponse.statusCode) {
+    throw new Error('Unexpected proxy response: Status code is missing!');
+  }
+  if (searchResponse.statusCode < 200 || searchResponse.statusCode >= 300) {
+    throw new HttpError(
+      `Unexpected status code: ${searchResponse.statusCode}`,
+      searchResponse.statusCode,
+      null,
+      searchResponse,
+    );
+  }
+
+  try {
+    return JSON.parse(searchResponse.body).packages as ArtifactHubTask[];
+  } catch (e) {
+    throw new Error('Failed to parse search response body as JSON');
+  }
+};
diff --git a/frontend/packages/pipelines-plugin/src/components/catalog/providers/useArtifactHubTasksProvider.tsx b/frontend/packages/pipelines-plugin/src/components/catalog/providers/useArtifactHubTasksProvider.tsx
index 2cc32f9bcf5..0b90a3b1726 100644
--- a/frontend/packages/pipelines-plugin/src/components/catalog/providers/useArtifactHubTasksProvider.tsx
+++ b/frontend/packages/pipelines-plugin/src/components/catalog/providers/useArtifactHubTasksProvider.tsx
@@ -6,8 +6,9 @@ import { referenceForModel } from '@console/internal/module/k8s';
 import { TaskModel } from '../../../models/pipelines';
 import { TaskProviders } from '../../pipelines/const';
 import { ARTIFACTHUB } from '../../quicksearch/const';
-import { ArtifactHubTask, useGetArtifactHubTasks } from '../apis/artifactHub';
+import { useGetArtifactHubTasks } from '../apis/artifactHub';
 import { TektonHubTask } from '../apis/tektonHub';
+import { ArtifactHubTask } from '../apis/utils';
 import { useTektonHubIntegration } from '../catalog-utils';
 
 const normalizeArtifactHubTasks = (artifactHubTasks: ArtifactHubTask[]): CatalogItem<any>[] => {
diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/utils/tekton-results.ts b/frontend/packages/pipelines-plugin/src/components/pipelineruns/utils/tekton-results.ts
index 8d0a4040984..01794ebb8c4 100644
--- a/frontend/packages/pipelines-plugin/src/components/pipelineruns/utils/tekton-results.ts
+++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/utils/tekton-results.ts
@@ -5,15 +5,15 @@ import {
   MatchLabels,
   Selector,
 } from '@console/dynamic-plugin-sdk/src';
-import { k8sGet } from '@console/internal/module/k8s';
+import { consoleFetchJSON } from '@console/dynamic-plugin-sdk/src/lib-core';
+import { HttpError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error';
 import { ALL_NAMESPACES_KEY } from '@console/shared/src/constants';
-import { consoleProxyFetch, consoleProxyFetchJSON } from '@console/shared/src/utils/proxy';
 import {
   DELETED_RESOURCE_IN_K8S_ANNOTATION,
   RESOURCE_LOADED_FROM_RESULTS_ANNOTATION,
 } from '../../../const';
-import { TektonResultModel } from '../../../models';
 import { PipelineRunKind, TaskRunKind } from '../../../types';
+import { DevConsoleEndpointResponse } from '../../catalog/apis/utils';
 
 // REST API spec
 // https://github.com/tektoncd/results/blob/main/docs/api/rest-api-spec.md
@@ -43,13 +43,13 @@ export type Log = {
   };
 };
 
-type ProxyRequest = {
-  allowInsecure?: boolean;
-  method: string;
-  url: string;
-  headers?: Record<string, string[]>;
-  queryparams?: Record<string, string[]>;
-  body?: string;
+type TRRequest = {
+  searchNamespace: string;
+  searchParams: string;
+};
+
+type TaskRunLogRequest = {
+  taskRunPath: string;
 };
 
 export type RecordsList = {
@@ -205,49 +205,58 @@ export const clearCache = () => {
 };
 const InFlightStore: { [key: string]: boolean } = {};
 
-export const getTRURLHost = async () => {
-  const tektonResult = await k8sGet(TektonResultModel, 'result');
-  const targetNamespace = tektonResult?.spec?.targetNamespace;
-  const serverPort = tektonResult?.spec?.server_port ?? '8080';
-  const tlsHostname = tektonResult?.spec?.tls_hostname_override;
-  let tektonResultsAPI;
-  if (tlsHostname) {
-    tektonResultsAPI = `${tlsHostname}:${serverPort}`;
-  } else if (targetNamespace && serverPort) {
-    tektonResultsAPI = `tekton-results-api-service.${targetNamespace}.svc.cluster.local:${serverPort}`;
-  } else {
-    tektonResultsAPI = `tekton-results-api-service.openshift-pipelines.svc.cluster.local:${serverPort}`;
-  }
-  return tektonResultsAPI;
-};
-
-export const createTektonResultsUrl = async (
+export const fetchTektonResultsURLConfig = async (
   namespace: string,
   dataType?: DataType,
   filter?: string,
   options?: TektonResultsOptions,
   nextPageToken?: string,
-): Promise<string> => {
-  const tektonResultsAPI = await getTRURLHost();
-  const namespaceToSearch = namespace && namespace !== ALL_NAMESPACES_KEY ? namespace : '-';
-  const url = `https://${tektonResultsAPI}/apis/results.tekton.dev/v1alpha2/parents/${namespaceToSearch}/results/-/records?${new URLSearchParams(
-    {
-      // default sort should always be by `create_time desc`
-      // order_by: 'create_time desc', not supported yet
-      page_size: `${Math.max(
-        MINIMUM_PAGE_SIZE,
-        Math.min(MAXIMUM_PAGE_SIZE, options?.limit >= 0 ? options.limit : options?.pageSize ?? 50),
-      )}`,
-      ...(nextPageToken ? { page_token: nextPageToken } : {}),
-      filter: AND(
-        EQ('data_type', dataType.toString()),
-        filter,
-        selectorToFilter(options?.selector),
-        options?.filter,
-      ),
-    },
-  ).toString()}`;
-  return url;
+): Promise<TRRequest> => {
+  const searchNamespace = namespace && namespace !== ALL_NAMESPACES_KEY ? namespace : '-';
+  const searchParams = `${new URLSearchParams({
+    // default sort should always be by `create_time desc`
+    // order_by: 'create_time desc', not supported yet
+    page_size: `${Math.max(
+      MINIMUM_PAGE_SIZE,
+      Math.min(MAXIMUM_PAGE_SIZE, options?.limit >= 0 ? options.limit : options?.pageSize ?? 50),
+    )}`,
+    ...(nextPageToken ? { page_token: nextPageToken } : {}),
+    filter: AND(
+      EQ('data_type', dataType.toString()),
+      filter,
+      selectorToFilter(options?.selector),
+      options?.filter,
+    ),
+  }).toString()}`;
+  return { searchNamespace, searchParams };
+};
+
+/**
+ * Fetches the Tekton results from the Tekton Results API.
+ */
+const fetchTektonResults = async (tRRequest: TRRequest): Promise<RecordsList> => {
+  const TEKTON_RESULTS_FETCH_URL = '/api/dev-console/tekton-results/get';
+  const resultListResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+    TEKTON_RESULTS_FETCH_URL,
+    tRRequest,
+  );
+
+  if (!resultListResponse.statusCode) {
+    throw new Error('Unexpected proxy response: Status code is missing!');
+  }
+  if (resultListResponse.statusCode < 200 || resultListResponse.statusCode >= 300) {
+    throw new HttpError(
+      `Unexpected status code: ${resultListResponse.statusCode}`,
+      resultListResponse.statusCode,
+      null,
+      resultListResponse,
+    );
+  }
+  try {
+    return JSON.parse(resultListResponse.body) as RecordsList;
+  } catch (e) {
+    throw new Error('Failed to parse task details response body as JSON');
+  }
 };
 
 export const getFilteredRecord = async <R extends K8sResourceCommon>(
@@ -277,11 +286,17 @@ export const getFilteredRecord = async <R extends K8sResourceCommon>(
   InFlightStore[cacheKey] = true;
   const value = await (async (): Promise<[R[], RecordsList]> => {
     try {
-      const url = await createTektonResultsUrl(namespace, dataType, filter, options, nextPageToken);
-      let list: RecordsList = await consoleProxyFetchJSON({
-        url,
-        method: 'GET',
-        allowInsecure: true,
+      const { searchNamespace, searchParams } = await fetchTektonResultsURLConfig(
+        namespace,
+        dataType,
+        filter,
+        options,
+        nextPageToken,
+      );
+
+      let list: RecordsList = await fetchTektonResults({
+        searchNamespace,
+        searchParams,
       });
       if (options?.limit >= 0) {
         list = {
@@ -391,20 +406,37 @@ const isJSONString = (str: string): boolean => {
   return true;
 };
 
-export const consoleProxyFetchLog = <T>(proxyRequest: ProxyRequest): Promise<T> => {
-  return consoleProxyFetch(proxyRequest).then((response) => {
-    return isJSONString(response.body) ? JSON.parse(response.body) : response.body;
-  });
+/**
+ * Fetches the task run logs from the Tekton Results API.
+ */
+const fetchTaskRunLogs = async <T>(taskRunLogRequest: TaskRunLogRequest): Promise<T> => {
+  const TEKTON_RESULTS_TASKRUN_LOGS_URL = '/api/dev-console/tekton-results/logs';
+  const taskRunLogResponse: DevConsoleEndpointResponse = await consoleFetchJSON.post(
+    TEKTON_RESULTS_TASKRUN_LOGS_URL,
+    taskRunLogRequest,
+  );
+
+  if (!taskRunLogResponse.statusCode) {
+    throw new Error('Unexpected proxy response: Status code is missing!');
+  }
+  if (taskRunLogResponse.statusCode < 200 || taskRunLogResponse.statusCode >= 300) {
+    throw new HttpError(
+      `Unexpected status code: ${taskRunLogResponse.statusCode}`,
+      taskRunLogResponse.statusCode,
+      null,
+      taskRunLogResponse,
+    );
+  }
+  return isJSONString(taskRunLogResponse.body)
+    ? JSON.parse(taskRunLogResponse.body)
+    : taskRunLogResponse.body;
 };
 
 export const getTaskRunLog = async (taskRunPath: string): Promise<string> => {
   if (!taskRunPath) {
     throw404();
   }
-  const tektonResultsAPI = await getTRURLHost();
-  const url = `https://${tektonResultsAPI}/apis/results.tekton.dev/v1alpha2/parents/${taskRunPath.replace(
-    '/records/',
-    '/logs/',
-  )}`;
-  return consoleProxyFetchLog({ url, method: 'GET', allowInsecure: true });
+  return fetchTaskRunLogs({
+    taskRunPath: taskRunPath.replace('/records/', '/logs/'),
+  });
 };
diff --git a/frontend/packages/pipelines-plugin/src/const.ts b/frontend/packages/pipelines-plugin/src/const.ts
index 0ec7f37d390..e3eca66d9c4 100644
--- a/frontend/packages/pipelines-plugin/src/const.ts
+++ b/frontend/packages/pipelines-plugin/src/const.ts
@@ -8,5 +8,6 @@ export const PIPELINE_STRATEGY_LABEL = 'pipeline.openshift.io/strategy';
 export const PREFERRED_DEV_PIPELINE_PAGE_TAB_USER_SETTING_KEY = 'pipeline.preferredPipelinePageTab';
 export const FUNC_PIPELINE_RUNTIME_LABEL = 'function.knative.dev/runtime';
 export const ARTIFACTHUB_API_BASE_URL = 'https://artifacthub.io/api/v1';
+export const GITHUB_BASE_URL = 'https://github.com';
 export const DELETED_RESOURCE_IN_K8S_ANNOTATION = 'resource.deleted.in.k8s';
 export const RESOURCE_LOADED_FROM_RESULTS_ANNOTATION = 'resource.loaded.from.tektonResults';
diff --git a/pkg/devconsole/artifacthub/artifacthub.go b/pkg/devconsole/artifacthub/artifacthub.go
new file mode 100644
index 00000000000..aa79fb682b2
--- /dev/null
+++ b/pkg/devconsole/artifacthub/artifacthub.go
@@ -0,0 +1,94 @@
+package artifacthub
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+const (
+	ARTIFACTHUB_API_BASE_URL string = "https://artifacthub.io/api/v1"
+	GITHUB_BASE_URL          string = "https://github.com"
+)
+
+func makeHTTPRequest(url string) (common.DevConsoleCommonResponse, error) {
+	serviceRequest, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	serviceTransport := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+	}
+
+	serviceClient := &http.Client{
+		Transport: serviceTransport,
+	}
+
+	serviceResponse, err := serviceClient.Do(serviceRequest)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to send request: %v", err)
+	}
+	defer serviceResponse.Body.Close()
+
+	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	return common.DevConsoleCommonResponse{
+		StatusCode: serviceResponse.StatusCode,
+		Headers:    serviceResponse.Header,
+		Body:       string(serviceResponseBody),
+	}, nil
+}
+
+func GetTaskYAMLFromGithub(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request TaskYAMLRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	GITHUB_TASK_YAML_URL := fmt.Sprintf("%s/%s",
+		GITHUB_BASE_URL,
+		request.YamlPath,
+	)
+	return makeHTTPRequest(GITHUB_TASK_YAML_URL)
+}
+
+func GetTaskDetails(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request TaskDetailsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	ARTIFACTHUB_TASKS_DETAILS_URL := fmt.Sprintf("%s/packages/tekton-task/%s/%s/%s",
+		ARTIFACTHUB_API_BASE_URL,
+		url.PathEscape(request.RepoName),
+		url.PathEscape(request.Name),
+		url.PathEscape(request.Version),
+	)
+	return makeHTTPRequest(ARTIFACTHUB_TASKS_DETAILS_URL)
+}
+
+func SearchTasks(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request SearchRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	ARTIFACTHUB_TASKS_SEARCH_URL := ARTIFACTHUB_API_BASE_URL + "/packages/search?offset=0&limit=60&facets=false&kind=7&deprecated=false&sort=relevance"
+	if request.SearchQuery != "" {
+		ARTIFACTHUB_TASKS_SEARCH_URL = fmt.Sprintf("%s&ts_query_web=%s", ARTIFACTHUB_TASKS_SEARCH_URL, url.PathEscape(request.SearchQuery))
+	}
+
+	return makeHTTPRequest(ARTIFACTHUB_TASKS_SEARCH_URL)
+}
diff --git a/pkg/devconsole/artifacthub/types.go b/pkg/devconsole/artifacthub/types.go
new file mode 100644
index 00000000000..4a09d2025d6
--- /dev/null
+++ b/pkg/devconsole/artifacthub/types.go
@@ -0,0 +1,34 @@
+package artifacthub
+
+type ArtifactHubRepository struct {
+	Name                    string `json:"name"`
+	Kind                    int    `json:"kind"`
+	URL                     string `json:"url"`
+	DisplayName             string `json:"display_name"`
+	RepositoryID            string `json:"repository_id"`
+	OrganizationName        string `json:"organization_name"`
+	OrganizationDisplayName string `json:"organization_display_name"`
+}
+
+type ArtifactHubTask struct {
+	PackageID   string                `json:"package_id"`
+	Name        string                `json:"name"`
+	Description string                `json:"description"`
+	Version     string                `json:"version"`
+	DisplayName string                `json:"display_name"`
+	Repository  ArtifactHubRepository `json:"repository"`
+}
+
+type SearchRequest struct {
+	SearchQuery string `json:"searchQuery"`
+}
+
+type TaskYAMLRequest struct {
+	YamlPath string `json:"yamlPath"`
+}
+
+type TaskDetailsRequest struct {
+	RepoName string `json:"repoName"`
+	Name     string `json:"name"`
+	Version  string `json:"version"`
+}
diff --git a/pkg/devconsole/common/types.go b/pkg/devconsole/common/types.go
new file mode 100644
index 00000000000..337da44b69e
--- /dev/null
+++ b/pkg/devconsole/common/types.go
@@ -0,0 +1,9 @@
+package common
+
+import "net/http"
+
+type DevConsoleCommonResponse struct {
+	StatusCode int         `json:"statusCode"`
+	Headers    http.Header `json:"headers"`
+	Body       string      `json:"body"`
+}
diff --git a/pkg/devconsole/handler.go b/pkg/devconsole/handler.go
new file mode 100644
index 00000000000..18f5528bc8b
--- /dev/null
+++ b/pkg/devconsole/handler.go
@@ -0,0 +1,94 @@
+package devconsole
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/artifacthub"
+	tektonresults "github.com/openshift/console/pkg/devconsole/tekton-results"
+	"github.com/openshift/console/pkg/devconsole/webhooks"
+	"github.com/openshift/console/pkg/serverutils"
+	"k8s.io/client-go/dynamic"
+)
+
+type handlerFunc func(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string, proxyHeaderDenyList []string) (interface{}, error)
+
+func handleRequest(w http.ResponseWriter, r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string, proxyHeaderDenyList []string, handler handlerFunc) {
+	response, err := handler(r, user, dynamicClient, k8sMode, proxyHeaderDenyList)
+	if err != nil {
+		serverutils.SendResponse(w, http.StatusInternalServerError, serverutils.ApiError{Err: err.Error()})
+		return
+	}
+	serverutils.SendResponse(w, http.StatusOK, response)
+}
+
+func Handler(user *auth.User, w http.ResponseWriter, r *http.Request, dynamicClient *dynamic.DynamicClient, k8sMode string, proxyHeaderDenyList []string) {
+	path := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
+	if len(path) != 2 {
+		serverutils.SendResponse(w, http.StatusNotFound, serverutils.ApiError{Err: "Invalid URL"})
+		return
+	}
+
+	// Define routes
+	routes := map[string]map[string]handlerFunc{
+		"artifacthub": {
+			// POST /api/dev-console/artifacthub/search
+			"search": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string, _ []string) (interface{}, error) {
+				return artifacthub.SearchTasks(r, user)
+			},
+			// POST /api/dev-console/artifacthub/get
+			"get": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string, _ []string) (interface{}, error) {
+				return artifacthub.GetTaskDetails(r, user)
+			},
+			// POST /api/dev-console/artifacthub/yaml
+			"yaml": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string, _ []string) (interface{}, error) {
+				return artifacthub.GetTaskYAMLFromGithub(r, user)
+			},
+		},
+		"tekton-results": {
+			// POST /api/dev-console/tekton-results/get
+			"get": func(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string, _ []string) (interface{}, error) {
+				return tektonresults.GetTektonResults(r, user, dynamicClient, k8sMode)
+			},
+			// POST /api/dev-console/tekton-results/logs
+			"logs": func(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string, _ []string) (interface{}, error) {
+				return tektonresults.GetTaskRunLog(r, user, dynamicClient, k8sMode)
+			},
+			// POST /api/dev-console/tekton-results/summary
+			"summary": func(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string, _ []string) (interface{}, error) {
+				return tektonresults.GetResultsSummary(r, user, dynamicClient, k8sMode)
+			},
+		},
+		"webhooks": {
+			// POST /api/dev-console/webhooks/github
+			"github": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string,
+				proxyHeaderDenyList []string) (interface{}, error) {
+
+				return webhooks.CreateGithubWebhook(r, user, proxyHeaderDenyList)
+			},
+			// POST /api/dev-console/webhooks/gitlab
+			"gitlab": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string, proxyHeaderDenyList []string) (interface{}, error) {
+				return webhooks.CreateGitlabWebhook(r, user, proxyHeaderDenyList)
+			},
+			// POST /api/dev-console/webhooks/bitbucket
+			"bitbucket": func(r *http.Request, user *auth.User, _ *dynamic.DynamicClient, _ string, proxyHeaderDenyList []string) (interface{}, error) {
+				return webhooks.CreateBitbucketWebhook(r, user, proxyHeaderDenyList)
+			},
+		},
+	}
+
+	// Check for valid route and method
+	if methodHandlers, ok := routes[path[0]]; ok {
+		if handler, ok := methodHandlers[path[1]]; ok {
+			if r.Method != http.MethodPost {
+				serverutils.SendResponse(w, http.StatusMethodNotAllowed, serverutils.ApiError{Err: "Invalid method: only POST is allowed"})
+				return
+			}
+			handleRequest(w, r, user, dynamicClient, k8sMode, proxyHeaderDenyList, handler)
+			return
+		}
+	}
+
+	serverutils.SendResponse(w, http.StatusNotFound, serverutils.ApiError{Err: "Invalid URL"})
+}
diff --git a/pkg/devconsole/proxy/proxy.go b/pkg/devconsole/proxy/proxy.go
deleted file mode 100644
index e0b574568d1..00000000000
--- a/pkg/devconsole/proxy/proxy.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package proxy
-
-import (
-	"crypto/tls"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"strings"
-
-	"github.com/openshift/console/pkg/auth"
-	"github.com/openshift/console/pkg/serverutils"
-)
-
-func Handler(user *auth.User, w http.ResponseWriter, r *http.Request) {
-	path := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
-
-	if len(path) == 2 && path[0] == "proxy" && path[1] == "internet" {
-		// POST  /api/dev-console/proxy/internet
-		if r.Method == http.MethodPost {
-			response, err := serve(r, user)
-			if err != nil {
-				serverutils.SendResponse(w, http.StatusInternalServerError, serverutils.ApiError{Err: err.Error()})
-				return
-			}
-			serverutils.SendResponse(w, http.StatusOK, response)
-			return
-		} else {
-			serverutils.SendResponse(w, http.StatusMethodNotAllowed, serverutils.ApiError{Err: "Invalid method: only POST is allowed"})
-			return
-		}
-	} else {
-		serverutils.SendResponse(w, http.StatusNotFound, serverutils.ApiError{Err: "Invalid URL"})
-		return
-	}
-}
-
-func serve(r *http.Request, user *auth.User) (ProxyResponse, error) {
-	var request ProxyRequest
-	err := json.NewDecoder(r.Body).Decode(&request)
-	if err != nil {
-		return ProxyResponse{}, fmt.Errorf("failed to parse request: %v", err)
-	}
-
-	if request.Method == "" {
-		request.Method = http.MethodGet
-	}
-
-	var serviceRequest *http.Request
-	if request.Body == "" {
-		serviceRequest, err = http.NewRequest(request.Method, request.Url, nil)
-	} else {
-		serviceRequest, err = http.NewRequest(request.Method, request.Url, strings.NewReader(request.Body))
-	}
-
-	if err != nil {
-		return ProxyResponse{}, fmt.Errorf("failed to create request: %v", err)
-	}
-
-	for key, values := range request.Headers {
-		for _, value := range values {
-			serviceRequest.Header.Add(key, value)
-		}
-	}
-
-	if request.AllowAuthHeader {
-		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
-	}
-
-	query := serviceRequest.URL.Query()
-	for key, values := range request.Queryparams {
-		for _, value := range values {
-			query.Add(key, value)
-		}
-	}
-	serviceRequest.URL.RawQuery = query.Encode()
-
-	var serviceTransport *http.Transport
-	if request.AllowInsecure {
-		serviceTransport = &http.Transport{
-			Proxy: http.ProxyFromEnvironment,
-			TLSClientConfig: &tls.Config{
-				InsecureSkipVerify: true,
-			},
-		}
-	} else {
-		serviceTransport = &http.Transport{
-			Proxy: http.ProxyFromEnvironment,
-		}
-	}
-	serviceClient := &http.Client{
-		Transport: serviceTransport,
-	}
-
-	serviceResponse, err := serviceClient.Do(serviceRequest)
-	if err != nil {
-		return ProxyResponse{}, fmt.Errorf("Failed to send request: %v", err)
-	}
-	defer serviceResponse.Body.Close()
-	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
-	if err != nil {
-		return ProxyResponse{}, fmt.Errorf("Failed to read response body: %v", err)
-	}
-
-	return ProxyResponse{
-		StatusCode: serviceResponse.StatusCode,
-		Headers:    serviceResponse.Header,
-		Body:       string(serviceResponseBody),
-	}, nil
-}
diff --git a/pkg/devconsole/proxy/proxy_test.go b/pkg/devconsole/proxy/proxy_test.go
deleted file mode 100644
index a035f212b6c..00000000000
--- a/pkg/devconsole/proxy/proxy_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package proxy
-
-import (
-	"bytes"
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"reflect"
-	"testing"
-
-	"github.com/openshift/console/pkg/auth"
-)
-
-var user = &auth.User{
-	ID:       "test-id",
-	Username: "test-user",
-	Token:    "test-token",
-}
-
-func TestProxyEndpoint(t *testing.T) {
-	tests := []struct {
-		testName         string
-		request          ProxyRequest
-		expectedResponse ProxyResponse
-	}{
-		{
-			testName: "valid GET",
-			request: ProxyRequest{
-				Url:    "testserver",
-				Method: http.MethodGet,
-			},
-			expectedResponse: ProxyResponse{
-				StatusCode: http.StatusOK,
-				Headers: http.Header{
-					"Content-Type":   {"application/json"},
-					"Content-Length": {"15"},
-					"Authorization":  {user.Token},
-				},
-				Body: "Mocked response",
-			},
-		},
-		{
-			testName: "valid GET without method",
-			request: ProxyRequest{
-				Url: "testserver",
-			},
-			expectedResponse: ProxyResponse{
-				StatusCode: http.StatusOK,
-				Headers: http.Header{
-					"Content-Type":   {"application/json"},
-					"Content-Length": {"15"},
-					"Authorization":  {user.Token},
-				},
-				Body: "Mocked response",
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.testName, func(t *testing.T) {
-
-			server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-				rw.Header().Add("Content-Type", "application/json")
-				rw.Header().Add("Authorization", user.Token)
-				rw.WriteHeader(http.StatusOK)
-				rw.Write([]byte("Mocked response"))
-			}))
-			defer server.Close()
-
-			tt.request.Url = server.URL
-			body, err := json.Marshal(tt.request)
-			if err != nil {
-				t.Errorf("Unexpected error: %v", err)
-			}
-
-			req, err := http.NewRequest(http.MethodPost, "/proxy", bytes.NewBuffer(body))
-			if err != nil {
-				t.Errorf("Unexpected error: %v", err)
-			}
-
-			actual, err := serve(req, user)
-			if err != nil {
-				t.Errorf("Unexpected error: %v", err)
-			}
-			actual.Headers.Del("Date")
-			if !reflect.DeepEqual(tt.expectedResponse, actual) {
-				t.Errorf("Response does not match expectation:\n%v\nbut got\n%v", tt.expectedResponse, actual)
-			}
-		})
-	}
-}
diff --git a/pkg/devconsole/proxy/types.go b/pkg/devconsole/proxy/types.go
deleted file mode 100644
index 8c03859db17..00000000000
--- a/pkg/devconsole/proxy/types.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package proxy
-
-import (
-	"net/http"
-	"net/url"
-)
-
-type ProxyRequest struct {
-	AllowInsecure   bool        `json:"allowInsecure,omitempty"`
-	AllowAuthHeader bool        `json:"allowAuthHeader,omitempty"`
-	Method          string      `json:"method"`
-	Url             string      `json:"url"`
-	Headers         http.Header `json:"headers"`
-	Queryparams     url.Values  `json:"queryparams"`
-	Body            string      `json:"body"`
-}
-
-type ProxyResponse struct {
-	StatusCode int         `json:"statusCode"`
-	Headers    http.Header `json:"headers"`
-	Body       string      `json:"body"`
-}
diff --git a/pkg/devconsole/tekton-results/results.go b/pkg/devconsole/tekton-results/results.go
new file mode 100644
index 00000000000..172ffbb0b79
--- /dev/null
+++ b/pkg/devconsole/tekton-results/results.go
@@ -0,0 +1,202 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/klog/v2"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+	tlsCertPath             string = "/var/serving-cert/tls.crt"
+)
+
+func getTRHost(dynamicClient *dynamic.DynamicClient, k8sMode string) (string, error) {
+	if k8sMode == "off-cluster" {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		klog.Errorf("spec.targetNamespace not found in TektonResults resource")
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		klog.Errorf("spec.server_port not found in TektonResults resource. Using default port 8080")
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		klog.Errorf("spec.tls_hostname_override not found in TektonResults resource")
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func makeHTTPRequest(url, userToken string) (common.DevConsoleCommonResponse, error) {
+	serviceRequest, err := http.NewRequest(http.MethodGet, url, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	// Needed for TektonResults API
+	serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userToken))
+
+	// Load the CA certificate
+	caCert, err := os.ReadFile(tlsCertPath)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read CA certificate: %v", err)
+	}
+
+	// Create a CertPool and add the CA certificate
+	caCertPool := x509.NewCertPool()
+	if !caCertPool.AppendCertsFromPEM(caCert) {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to append CA certificate")
+	}
+
+	serviceTransport := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		TLSClientConfig: &tls.Config{
+			RootCAs: caCertPool,
+		},
+	}
+
+	serviceClient := &http.Client{
+		Transport: serviceTransport,
+	}
+
+	serviceResponse, err := serviceClient.Do(serviceRequest)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to send request: %v", err)
+	}
+	defer serviceResponse.Body.Close()
+	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	return common.DevConsoleCommonResponse{
+		StatusCode: serviceResponse.StatusCode,
+		Headers:    serviceResponse.Header,
+		Body:       string(serviceResponseBody),
+	}, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+	TEKTON_RESULTS_HOST, err := getTRHost(dynamicClient, k8sMode)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	parsedParams, err := url.ParseQuery(request.SearchParams)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("error parsing search params: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s",
+		TEKTON_RESULTS_HOST,
+		url.PathEscape(request.SearchNamespace),
+		parsedParams.Encode(),
+	)
+
+	return makeHTTPRequest(TEKTON_RESULTS_URL, user.Token)
+}
+
+func GetResultsSummary(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string) (common.DevConsoleCommonResponse, error) {
+	var request SummaryRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+	TEKTON_RESULTS_HOST, err := getTRHost(dynamicClient, k8sMode)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	parsedParams, err := url.ParseQuery(request.SearchParams)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("error parsing search params: %v", err)
+	}
+
+	SUMMARY_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records/summary?%s",
+		TEKTON_RESULTS_HOST,
+		url.PathEscape(request.SearchNamespace),
+		parsedParams.Encode(),
+	)
+
+	return makeHTTPRequest(SUMMARY_URL, user.Token)
+}
+
+func GetTaskRunLog(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, k8sMode string) (common.DevConsoleCommonResponse, error) {
+	var request TaskRunLogRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+	TEKTON_RESULTS_HOST, err := getTRHost(dynamicClient, k8sMode)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TASKRUN_LOG_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s",
+		TEKTON_RESULTS_HOST,
+		request.TaskRunPath,
+	)
+
+	return makeHTTPRequest(TASKRUN_LOG_URL, user.Token)
+}
diff --git a/pkg/devconsole/tekton-results/types.go b/pkg/devconsole/tekton-results/types.go
new file mode 100644
index 00000000000..9c11473e847
--- /dev/null
+++ b/pkg/devconsole/tekton-results/types.go
@@ -0,0 +1,15 @@
+package tektonresults
+
+type TektonResultsRequest struct {
+	SearchNamespace string `json:"searchNamespace"`
+	SearchParams    string `json:"searchParams"`
+}
+
+type SummaryRequest struct {
+	SearchNamespace string `json:"searchNamespace"`
+	SearchParams    string `json:"searchParams"`
+}
+
+type TaskRunLogRequest struct {
+	TaskRunPath string `json:"taskRunPath"`
+}
diff --git a/pkg/devconsole/webhooks/types.go b/pkg/devconsole/webhooks/types.go
new file mode 100644
index 00000000000..2aa712e2d9b
--- /dev/null
+++ b/pkg/devconsole/webhooks/types.go
@@ -0,0 +1,56 @@
+package webhooks
+
+import (
+	"net/http"
+)
+
+type GHWebhookBody struct {
+	Name   string `json:"name"`
+	Active bool   `json:"active"`
+	Config struct {
+		URL         string `json:"url"`
+		ContentType string `json:"content_type"`
+		InsecureSSL string `json:"insecure_ssl"`
+		Secret      string `json:"secret"`
+	} `json:"config"`
+	Events []string `json:"events"`
+}
+
+type GithubWebhookRequest struct {
+	Headers  http.Header   `json:"headers"`
+	HostName string        `json:"hostName"`
+	Owner    string        `json:"owner"`
+	RepoName string        `json:"repoName"`
+	Body     GHWebhookBody `json:"body"`
+}
+
+type GLWebhookBody struct {
+	URL                   string `json:"url"`
+	PushEvents            bool   `json:"push_events"`
+	MergeRequestsEvents   bool   `json:"merge_requests_events"`
+	EnableSSLVerification bool   `json:"enable_ssl_verification"`
+	Token                 string `json:"token"`
+}
+
+type GitlabWebhookRequest struct {
+	Headers   http.Header   `json:"headers"`
+	HostName  string        `json:"hostName"`
+	ProjectID string        `json:"projectID"`
+	Body      GLWebhookBody `json:"body"`
+}
+
+type BBWebhookBody struct {
+	URL                  string   `json:"url"`
+	Events               []string `json:"events"`
+	SkipCertVerification bool     `json:"skip_cert_verification"`
+	Active               bool     `json:"active"`
+}
+
+type BitbucketWebhookRequest struct {
+	Headers  http.Header   `json:"headers"`
+	IsServer bool          `json:"isServer"`
+	BaseURL  string        `json:"baseURL"`
+	Owner    string        `json:"owner"`
+	RepoName string        `json:"repoName"`
+	Body     BBWebhookBody `json:"body"`
+}
diff --git a/pkg/devconsole/webhooks/webhooks.go b/pkg/devconsole/webhooks/webhooks.go
new file mode 100644
index 00000000000..ab2e94c2af6
--- /dev/null
+++ b/pkg/devconsole/webhooks/webhooks.go
@@ -0,0 +1,131 @@
+package webhooks
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"slices"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func makeHTTPRequest(url string, headers http.Header, body []byte, proxyHeaderDenyList []string) (common.DevConsoleCommonResponse, error) {
+	serviceRequest, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range headers {
+		if slices.Contains(proxyHeaderDenyList, key) {
+			continue
+		}
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
+
+	serviceTransport := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+	}
+
+	serviceClient := &http.Client{
+		Transport: serviceTransport,
+	}
+
+	serviceResponse, err := serviceClient.Do(serviceRequest)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to send request: %v", err)
+	}
+	defer serviceResponse.Body.Close()
+	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	return common.DevConsoleCommonResponse{
+		StatusCode: serviceResponse.StatusCode,
+		Headers:    serviceResponse.Header,
+		Body:       string(serviceResponseBody),
+	}, nil
+}
+
+func CreateGithubWebhook(r *http.Request, user *auth.User, proxyHeaderDenyList []string) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks",
+		request.HostName,
+		url.PathEscape(request.Owner),
+		url.PathEscape(request.RepoName),
+	)
+
+	return makeHTTPRequest(GH_WEBHOOK_URL, request.Headers, bodyBytes, proxyHeaderDenyList)
+
+}
+
+func CreateGitlabWebhook(r *http.Request, user *auth.User, proxyHeaderDenyList []string) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/gitlab
+
+	var request GitlabWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GL_WEBHOOK_URL := fmt.Sprintf("%s/api/v4/projects/%s/hooks",
+		request.HostName,
+		url.PathEscape(request.ProjectID),
+	)
+
+	return makeHTTPRequest(GL_WEBHOOK_URL, request.Headers, bodyBytes, proxyHeaderDenyList)
+}
+
+func CreateBitbucketWebhook(r *http.Request, user *auth.User, proxyHeaderDenyList []string) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/bitbucket
+
+	var request BitbucketWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	var BB_WEBHOOK_URL string
+	if request.IsServer {
+		BB_WEBHOOK_URL = fmt.Sprintf("%s/projects/%s/repos/%s/hooks",
+			request.BaseURL,
+			url.PathEscape(request.Owner),
+			url.PathEscape(request.RepoName),
+		)
+	} else {
+		BB_WEBHOOK_URL = fmt.Sprintf("%s/repositories/%s/%s/hooks",
+			request.BaseURL,
+			url.PathEscape(request.Owner),
+			url.PathEscape(request.RepoName),
+		)
+	}
+
+	return makeHTTPRequest(BB_WEBHOOK_URL, request.Headers, bodyBytes, proxyHeaderDenyList)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index f81fdc1edce..30076406b7a 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -25,7 +25,7 @@ import (
 	"github.com/openshift/console/pkg/auth"
 	"github.com/openshift/console/pkg/auth/csrfverifier"
 	"github.com/openshift/console/pkg/auth/sessions"
-	devconsoleProxy "github.com/openshift/console/pkg/devconsole/proxy"
+	devconsole "github.com/openshift/console/pkg/devconsole"
 	"github.com/openshift/console/pkg/devfile"
 	gql "github.com/openshift/console/pkg/graphql"
 	"github.com/openshift/console/pkg/graphql/resolver"
@@ -172,6 +172,7 @@ type Server struct {
 	AnonymousInternalProxiedK8SRT       http.RoundTripper
 	K8sMode                             string
 	K8sProxyConfig                      *proxy.Config
+	ProxyHeaderDenyList                 []string
 	KnativeChannelCRDLister             ResourceLister
 	KnativeEventSourceCRDLister         ResourceLister
 	KubeAPIServerURL                    string // JS global only. Not used for proxying.
@@ -476,11 +477,11 @@ func (s *Server) HTTPHandler() (http.Handler, error) {
 	handle("/api/console/knative-event-sources", authHandler(s.handleKnativeEventSourceCRDs))
 	handle("/api/console/knative-channels", authHandler(s.handleKnativeChannelCRDs))
 
-	// Dev-Console Proxy
+	// Dev-Console Endpoints
 	handle(devConsoleEndpoint, http.StripPrefix(
 		proxy.SingleJoiningSlash(s.BaseURL.Path, devConsoleEndpoint),
 		authHandlerWithUser(func(user *auth.User, w http.ResponseWriter, r *http.Request) {
-			devconsoleProxy.Handler(user, w, r)
+			devconsole.Handler(user, w, r, internalProxiedDynamic, s.K8sMode, s.ProxyHeaderDenyList)
 		})),
 	)
 


Reviews:
user: login: logonoff
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-24T13:46:01Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-18T18:12:15Z
user: login: knobunc
state: COMMENTED
author_association: NONE
submitted_at: 2024-11-18T18:33:03Z
user: login: knobunc
state: COMMENTED
author_association: NONE
submitted_at: 2024-11-18T18:36:14Z
user: login: knobunc
state: COMMENTED
author_association: NONE
submitted_at: 2024-11-18T18:38:09Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-25T13:07:36Z
user: login: knobunc
state: COMMENTED
author_association: NONE
submitted_at: 2024-11-25T13:58:11Z
user: login: vikram-raj
body: Verified it on cluster. ArtifactHub requests are working fine
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-31T14:58:18Z
user: login: vikram-raj
body: Verified it. All APIs using proxy are working as expected.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-12T10:40:41Z
user: login: spadgett
body: Thanks, @Lucifergene. I have some comments on the backend code, largely concerns about how we're handling tokens and insecure requests. I haven't looked at the frontend code.

Holding to make sure this doesn't accidentally merge until we can work through those concerns.

/hold
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-12T21:26:02Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T11:54:47Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T11:55:09Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T13:00:07Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T13:01:51Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T13:07:45Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-19T16:40:39Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-19T16:54:36Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-19T17:00:27Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-19T17:02:20Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-19T17:09:08Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T19:33:53Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-20T06:07:21Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-20T06:16:02Z
user: login: spadgett
body: Thanks @Lucifergene, this is looking good. Appreciate your patience. I've only reviewed the backend code.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T13:31:41Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:48:31Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:53:14Z
user: login: spadgett
body: /approve

I approve the change, just a couple small review comments
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T17:02:08Z
user: login: Lucifergene
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T17:04:41Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T18:07:25Z
user: login: lokanandaprabhu
body: /lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T06:48:08Z
user: login: spadgett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T14:17:54Z

Review comments:
diff_hunk: @@ -83,9 +73,11 @@ func TestProxyEndpoint(t *testing.T) {
 				t.Errorf("Unexpected error: %v", err)
 			}
 			actual.Headers.Del("Date")
-			if !reflect.DeepEqual(tt.expectedResponse, actual) {
+			if !reflect.DeepEqual(tt.expectedResponse.Body, actual.Body) {
 				t.Errorf("Response does not match expectation:\n%v\nbut got\n%v", tt.expectedResponse, actual)
 			}
 		})
 	}
 }
+
+var apiBody = "{\"package_id\":\"b1a4ba18-5b16-44ce-b2ac-6192076a7338\",\"name\":\"tekton-pipeline\",\"normalized_name\":\"tekton-pipeline\",\"is_operator\":false,\"description\":\"A Helm chart for Tekton Pipelines\",\"logo_image_id\":\"daa05ffa-5ae0-4dbf-b0a6-2483fa5ad82a\",\"home_url\":\"https://github.com/cdfoundation/tekton-helm-chart\",\"readme\":\"# tekton helm chart\\n\\n## Setup\\nFirst add the Jenkins X chart repository\\n\\n```sh\\nhelm repo add cdf https://cdfoundation.github.io/tekton-helm-chart/\\n```\\nIf it already exists be sure to update the local cache\\n```\\nhelm repo update\\n```\\n\\n## Basic install\\n```\\nhelm upgrade --install tekton cdf/tekton-pipeline\\n```\\n\\n## Authenticated Git requests\\nIf you are working with private git repositories or require secrets to tag or perform remote git actions then you can provide basic authentication which will be automatically mounted into Tekton Pipline pods.  We recommend using a bot user and a personal access token.\\n\\nGitHub Example:\\n\\n```sh\\nhelm upgrade --install --set auth.git.username=bot-user --set auth.git.password=123456abcdef --set auth.git.url=https://github.com tekton jenkins-x/tekton \\n```\\n## Authenticated Docker registries\\nIf you are pushing images to authenticated docker registries you can provide basic authentication which will be automatically mounted into Knative Tekton Pipline pods.\\n\\nDockerHub Example:\\n\\n```sh\\nhelm upgrade --install --set auth.docker.username=fred --set auth.docker.password=flintstone --set auth.docker.url=https://index.docker.io/v1/  tekton jenkins-x/tekton \\n```\\n## Configuration options\\n\\n|       Parameter                 |           Description                             |                         Default                                                 |\\n|----------------------------------------|---------------------------------------------------|--------------------------------------------------------------------------|\\n| `auth.git.username`             | Optional basic auth username for git provider     | ``                                                                              |\\n| `auth.git.password`             | Optional basic auth password for git provider     | ``                                                                              |\\n| `auth.git.url`                  | Optional basic auth server for git provider       | `https://github.com`                                                            |\\n| `auth.docker.username`          | Optional basic auth username for docker registry  | ``                                                                              |\\n| `auth.docker.password`          | Optional basic auth password for docker registry  | ``                                                                              |\\n| `auth.docker.url`               | Optional basic auth server for docker registry    | `https://index.docker.io/v1/`                                                   |\\n| `image.tag`                     | Docker image tag                                  | `see latest values.yaml`                                                        |\\n| `image.kubeconfigwriter`        | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/kubeconfigwriter` |\\n| `image.credsinit`               | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/creds-init`       |\\n| `image.gitinit`                 | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/git-init`         |\\n| `image.nop`                     | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/nop`              |\\n| `image.bash`                    | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/bash`             |\\n| `image.gsutil`                  | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/gsutil`           |\\n| `image.controller`              | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/controller`       |\\n| `image.webhook`                 | Docker image                                      | `gcr.io/knative-nightly/github.com/knative/build-pipeline/cmd/webhook`          |\\n\",\"data\":{\"type\":\"\",\"apiVersion\":\"v1\",\"kubeVersion\":\"\"},\"version\":\"1.0.2\",\"available_versions\":[{\"version\":\"1.0.2\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1690543907},{\"version\":\"1.0.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1689932789},{\"version\":\"1.0.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1689768818},{\"version\":\"0.6.4\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1688562843},{\"version\":\"0.6.2\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1687856517},{\"version\":\"0.6.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1687854769},{\"version\":\"0.6.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1687448652},{\"version\":\"0.5.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1669397545},{\"version\":\"0.4.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1669334808},{\"version\":\"0.3.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1667528976},{\"version\":\"0.3.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1667227422},{\"version\":\"0.2.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1658261328},{\"version\":\"0.1.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1657934348},{\"version\":\"0.29.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647473788},{\"version\":\"0.27.2\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647473341},{\"version\":\"0.23.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.24.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.24.2\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.0.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.24.4\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.25.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.26.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.26.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.27.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.27.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.24.3\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.0.2\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.0.3\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.0.4\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.0.5\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.17.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.17.3\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.18.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.18.0-1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.19.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.19.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.20.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.21.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.21.1\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677},{\"version\":\"0.22.0\",\"contains_security_updates\":false,\"prerelease\":false,\"ts\":1647354677}],\"app_version\":\"0.42.0\",\"digest\":\"d29112db2db28b756d39a35941f3039df6848a700ccc25cd8d99d3bf7e5717cd\",\"deprecated\":false,\"contains_security_updates\":false,\"prerelease\":false,\"signed\":false,\"content_url\":\"https://cdfoundation.github.io/tekton-helm-chart/tekton-pipeline-1.0.2.tgz\",\"containers_images\":[{\"name\":\"\",\"image\":\"gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/controller:v0.42.0@sha256:1fa50403c071b768984e23e26d0e68d2f7e470284ef2eb73581ec556bacdad95\",\"whitelisted\":false},{\"name\":\"\",\"image\":\"gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/resolvers:v0.42.0@sha256:eaa7d21d45f0bc1c411823d6a943e668c820f9cf52f1549d188edb89e992f6e0\",\"whitelisted\":false},{\"name\":\"\",\"image\":\"gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/webhook:v0.42.0@sha256:90989eeb6e0ba9c481b1faba3b01bcc70725baa58484c8f6ce9d22cc601e63dc\",\"whitelisted\":false}],\"all_containers_images_whitelisted\":false,\"has_values_schema\":false,\"has_changelog\":false,\"ts\":1690543907,\"repository\":{\"repository_id\":\"0648b8bf-a545-45d7-ada4-2f846b1968f7\",\"name\":\"tekton\",\"display_name\":\"tekton\",\"url\":\"https://cdfoundation.github.io/tekton-helm-chart/\",\"private\":false,\"kind\":0,\"verified_publisher\":false,\"official\":false,\"scanner_disabled\":true,\"organization_name\":\"webofmars\",\"organization_display_name\":\"webofmars\"},\"stats\":{\"subscriptions\":0,\"webhooks\":0},\"production_organizations_count\":0}"
path: pkg/devconsole/proxy/proxy_test.go
user: login: logonoff
body: nit: should this be const?
created_at: 2024-10-24T13:46:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
+	time.Sleep(2 * time.Second)
+	ips2, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname after delay: %v", err)
+	}
+
+	// Ensure IP consistency between the two resolutions
+	for i := range ips {
+		if !ips[i].Equal(ips2[i]) {
+			return nil, fmt.Errorf("potential DNS rebinding detected")
+		}
+	}
+
+	return ips, nil
+}
+
+// Validate URL for SSRF protection
+func validateRequestURL(requestUrl string) error {
+
+	// Check if the URL is whitelisted
+	if isAllowedURL(requestUrl) {
+		return nil
+	}
+
+	// Parse the provided URL
+	parsedUrl, err := url.Parse(requestUrl)
+	if err != nil {
+		return fmt.Errorf("failed to parse URL: %v", err)
+	}
+
+	// Check if scheme is missing, and prepend "https://"
+	if parsedUrl.Scheme == "" {
+		parsedUrl.Scheme = "https"
+	}
+
+	// Allow only http or https schemes
+	if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
+		return fmt.Errorf("only http or https schemes are allowed")
+	}
+
+	// Disallow certain schemes via URL encoding tricks or unexpected cases
+	normalizedScheme := strings.ToLower(parsedUrl.Scheme)
+	if normalizedScheme != "http" && normalizedScheme != "https" {
+		return fmt.Errorf("invalid scheme detected: %s", parsedUrl.Scheme)
+	}
+
+	// Resolve the hostname to IP addresses and check DNS rebinding
+	ips, err := resolveHostname(parsedUrl.Hostname())
+	if err != nil {
+		return fmt.Errorf("failed to resolve IPs for hostname: %v", err)
+	}
+
+	// Check if any resolved IP is private, loopback, or link-local (i.e., non-public)
+	for _, ip := range ips {
+		if !isPublicIP(ip) {
+			return fmt.Errorf("access to private or internal IP detected")
+		}
+	}
+
+	// Disallow internal or local network hostnames (e.g., localhost, cluster.local)
+	if parsedUrl.Hostname() == "localhost" ||
+		strings.HasSuffix(parsedUrl.Hostname(), ".cluster.local") {
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: spadgett
body: I don't think this is sufficient because you can access internal services still by leaving off `.cluster.local` (for instance, just using hostname `my-service`). I'm not sure offhand to catch this case, however. We might need to talk to the networking team.
created_at: 2024-11-18T18:09:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
+	time.Sleep(2 * time.Second)
+	ips2, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname after delay: %v", err)
+	}
+
+	// Ensure IP consistency between the two resolutions
+	for i := range ips {
+		if !ips[i].Equal(ips2[i]) {
+			return nil, fmt.Errorf("potential DNS rebinding detected")
+		}
+	}
+
+	return ips, nil
+}
+
+// Validate URL for SSRF protection
+func validateRequestURL(requestUrl string) error {
+
+	// Check if the URL is whitelisted
+	if isAllowedURL(requestUrl) {
+		return nil
+	}
+
+	// Parse the provided URL
+	parsedUrl, err := url.Parse(requestUrl)
+	if err != nil {
+		return fmt.Errorf("failed to parse URL: %v", err)
+	}
+
+	// Check if scheme is missing, and prepend "https://"
+	if parsedUrl.Scheme == "" {
+		parsedUrl.Scheme = "https"
+	}
+
+	// Allow only http or https schemes
+	if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
+		return fmt.Errorf("only http or https schemes are allowed")
+	}
+
+	// Disallow certain schemes via URL encoding tricks or unexpected cases
+	normalizedScheme := strings.ToLower(parsedUrl.Scheme)
+	if normalizedScheme != "http" && normalizedScheme != "https" {
+		return fmt.Errorf("invalid scheme detected: %s", parsedUrl.Scheme)
+	}
+
+	// Resolve the hostname to IP addresses and check DNS rebinding
+	ips, err := resolveHostname(parsedUrl.Hostname())
+	if err != nil {
+		return fmt.Errorf("failed to resolve IPs for hostname: %v", err)
+	}
+
+	// Check if any resolved IP is private, loopback, or link-local (i.e., non-public)
+	for _, ip := range ips {
+		if !isPublicIP(ip) {
+			return fmt.Errorf("access to private or internal IP detected")
+		}
+	}
+
+	// Disallow internal or local network hostnames (e.g., localhost, cluster.local)
+	if parsedUrl.Hostname() == "localhost" ||
+		strings.HasSuffix(parsedUrl.Hostname(), ".cluster.local") {
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: knobunc
body: If you want to block a cluster resource, I think you need to resolved the same name and then check the ip address returned against the cluster pod network CIDRs and the service CIDRs...

But I am not quite sure what the threat is that you are trying to block here.
created_at: 2024-11-18T18:32:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
+	time.Sleep(2 * time.Second)
+	ips2, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname after delay: %v", err)
+	}
+
+	// Ensure IP consistency between the two resolutions
+	for i := range ips {
+		if !ips[i].Equal(ips2[i]) {
+			return nil, fmt.Errorf("potential DNS rebinding detected")
+		}
+	}
+
+	return ips, nil
+}
+
+// Validate URL for SSRF protection
+func validateRequestURL(requestUrl string) error {
+
+	// Check if the URL is whitelisted
+	if isAllowedURL(requestUrl) {
+		return nil
+	}
+
+	// Parse the provided URL
+	parsedUrl, err := url.Parse(requestUrl)
+	if err != nil {
+		return fmt.Errorf("failed to parse URL: %v", err)
+	}
+
+	// Check if scheme is missing, and prepend "https://"
+	if parsedUrl.Scheme == "" {
+		parsedUrl.Scheme = "https"
+	}
+
+	// Allow only http or https schemes
+	if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
+		return fmt.Errorf("only http or https schemes are allowed")
+	}
+
+	// Disallow certain schemes via URL encoding tricks or unexpected cases
+	normalizedScheme := strings.ToLower(parsedUrl.Scheme)
+	if normalizedScheme != "http" && normalizedScheme != "https" {
+		return fmt.Errorf("invalid scheme detected: %s", parsedUrl.Scheme)
+	}
+
+	// Resolve the hostname to IP addresses and check DNS rebinding
+	ips, err := resolveHostname(parsedUrl.Hostname())
+	if err != nil {
+		return fmt.Errorf("failed to resolve IPs for hostname: %v", err)
+	}
+
+	// Check if any resolved IP is private, loopback, or link-local (i.e., non-public)
+	for _, ip := range ips {
+		if !isPublicIP(ip) {
+			return fmt.Errorf("access to private or internal IP detected")
+		}
+	}
+
+	// Disallow internal or local network hostnames (e.g., localhost, cluster.local)
+	if parsedUrl.Hostname() == "localhost" ||
+		strings.HasSuffix(parsedUrl.Hostname(), ".cluster.local") {
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: knobunc
body: Oh, and NetworkPolicy may be a better fit if the pod can not talk to anything in the cluster (or to only specific things)
created_at: 2024-11-18T18:36:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: knobunc
body: If this is being resolved by a pod, using the default resolver, we force a min ttl of 30 seconds anyway, so this won't ever matter.

But it can't hurt (other than adding a 2s delay) in case that behavior changes in the future.
created_at: 2024-11-18T18:38:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
+	time.Sleep(2 * time.Second)
+	ips2, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname after delay: %v", err)
+	}
+
+	// Ensure IP consistency between the two resolutions
+	for i := range ips {
+		if !ips[i].Equal(ips2[i]) {
+			return nil, fmt.Errorf("potential DNS rebinding detected")
+		}
+	}
+
+	return ips, nil
+}
+
+// Validate URL for SSRF protection
+func validateRequestURL(requestUrl string) error {
+
+	// Check if the URL is whitelisted
+	if isAllowedURL(requestUrl) {
+		return nil
+	}
+
+	// Parse the provided URL
+	parsedUrl, err := url.Parse(requestUrl)
+	if err != nil {
+		return fmt.Errorf("failed to parse URL: %v", err)
+	}
+
+	// Check if scheme is missing, and prepend "https://"
+	if parsedUrl.Scheme == "" {
+		parsedUrl.Scheme = "https"
+	}
+
+	// Allow only http or https schemes
+	if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
+		return fmt.Errorf("only http or https schemes are allowed")
+	}
+
+	// Disallow certain schemes via URL encoding tricks or unexpected cases
+	normalizedScheme := strings.ToLower(parsedUrl.Scheme)
+	if normalizedScheme != "http" && normalizedScheme != "https" {
+		return fmt.Errorf("invalid scheme detected: %s", parsedUrl.Scheme)
+	}
+
+	// Resolve the hostname to IP addresses and check DNS rebinding
+	ips, err := resolveHostname(parsedUrl.Hostname())
+	if err != nil {
+		return fmt.Errorf("failed to resolve IPs for hostname: %v", err)
+	}
+
+	// Check if any resolved IP is private, loopback, or link-local (i.e., non-public)
+	for _, ip := range ips {
+		if !isPublicIP(ip) {
+			return fmt.Errorf("access to private or internal IP detected")
+		}
+	}
+
+	// Disallow internal or local network hostnames (e.g., localhost, cluster.local)
+	if parsedUrl.Hostname() == "localhost" ||
+		strings.HasSuffix(parsedUrl.Hostname(), ".cluster.local") {
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: Lucifergene
body: > If you want to block a cluster resource, I think you need to resolved the same name and then check the ip address returned against the cluster pod network CIDRs and the service CIDRs...

@knobunc I think this is already been handled in the previous checks. 
https://github.com/openshift/console/blob/5492c773ce38cd9f1612017cf6a61aaeab66c8c8/pkg/devconsole/proxy/ssrf_utils.go#L118

created_at: 2024-11-25T13:07:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,142 @@
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// List of allowed URLs for SSRF protection
+var allowedURLs = []string{
+	// ArtifactHub Public API
+	"https://artifacthub.io/api/v1",
+
+	// Tekton Results installed in the cluster
+	"http://tekton-results-api-service",
+	"https://tekton-results-api-service",
+}
+
+func isAllowedURL(requestURL string) bool {
+	for _, allowedURL := range allowedURLs {
+		if strings.HasPrefix(requestURL, allowedURL) {
+			return true
+		}
+	}
+	return false
+}
+
+// Helper function to check if an IP is private
+func isPrivateIP(ip net.IP) bool {
+	privateIPBlocks := []*net.IPNet{
+		// IPv4 private ranges (RFC 1918)
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv4 link-local addresses (169.254.0.0/16)
+		{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 link-local addresses (fe80::/10)
+		{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
+		// IPv6 private range (fc00::/7)
+		{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
+		// IPv6 loopback address (::1)
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateIPBlocks {
+		if block.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Whitelist-based check to only allow public IP addresses (e.g., from the internet)
+func isPublicIP(ip net.IP) bool {
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || isPrivateIP(ip) {
+		return false
+	}
+	return true
+}
+
+// Helper function to resolve the DNS hostname and check consistency
+func resolveHostname(hostname string) ([]net.IP, error) {
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname: %v", err)
+	}
+
+	// Introduce DNS rebinding protection: re-check the IPs after a small delay to prevent DNS rebinding
+	time.Sleep(2 * time.Second)
+	ips2, err := net.LookupIP(hostname)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve hostname after delay: %v", err)
+	}
+
+	// Ensure IP consistency between the two resolutions
+	for i := range ips {
+		if !ips[i].Equal(ips2[i]) {
+			return nil, fmt.Errorf("potential DNS rebinding detected")
+		}
+	}
+
+	return ips, nil
+}
+
+// Validate URL for SSRF protection
+func validateRequestURL(requestUrl string) error {
+
+	// Check if the URL is whitelisted
+	if isAllowedURL(requestUrl) {
+		return nil
+	}
+
+	// Parse the provided URL
+	parsedUrl, err := url.Parse(requestUrl)
+	if err != nil {
+		return fmt.Errorf("failed to parse URL: %v", err)
+	}
+
+	// Check if scheme is missing, and prepend "https://"
+	if parsedUrl.Scheme == "" {
+		parsedUrl.Scheme = "https"
+	}
+
+	// Allow only http or https schemes
+	if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
+		return fmt.Errorf("only http or https schemes are allowed")
+	}
+
+	// Disallow certain schemes via URL encoding tricks or unexpected cases
+	normalizedScheme := strings.ToLower(parsedUrl.Scheme)
+	if normalizedScheme != "http" && normalizedScheme != "https" {
+		return fmt.Errorf("invalid scheme detected: %s", parsedUrl.Scheme)
+	}
+
+	// Resolve the hostname to IP addresses and check DNS rebinding
+	ips, err := resolveHostname(parsedUrl.Hostname())
+	if err != nil {
+		return fmt.Errorf("failed to resolve IPs for hostname: %v", err)
+	}
+
+	// Check if any resolved IP is private, loopback, or link-local (i.e., non-public)
+	for _, ip := range ips {
+		if !isPublicIP(ip) {
+			return fmt.Errorf("access to private or internal IP detected")
+		}
+	}
+
+	// Disallow internal or local network hostnames (e.g., localhost, cluster.local)
+	if parsedUrl.Hostname() == "localhost" ||
+		strings.HasSuffix(parsedUrl.Hostname(), ".cluster.local") {
path: pkg/devconsole/proxy/ssrf_utils.go
user: login: knobunc
body: @Lucifergene where does that resolved ip get checked against the pod cidrs and the service cirds?  You also need to decide if the host addresses should be allowed and include them too
Honestly, this feels like somewhat of a weak answer anyway because why would cluster resources be forbidden, but still allow other hosts on an internal network that the cluster is part of?

Isn't the scenario that someone on the wider internet is able to access the console.  If the console can proxy stuff, an attacker can use it to access resources that the host can access, but that they should not.

The cluster is a good example of things they should not access, but the cluster is likely on an internal network behind a selective firewall... and this allows a full bypass.

My vote for the backport is to put a network policy on that blocks outgoing traffic and tell them that the admin needs to add policies to allow the sites they want access to.  It will break stuff though, so maybe have a few default allowed sites?  But remember that network policy will not allow you to deny anything.  Once a network policy is associated with the pod then it establishes a default deny, and the rules you define are what is allowed.  (Also, ingress and egress are handled independently, so an ingress policy will not establish a deny for egress).
created_at: 2024-11-25T13:58:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,185 @@
+package artifacthub
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+const (
+	ARTIFACTHUB_API_BASE_URL string = "https://artifacthub.io/api/v1"
+	GITHUB_BASE_URL          string = "https://github.com"
+)
+
+func GetTaskYAMLFromGithub(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request TaskYAMLRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	GITHUB_TASK_YAML_URL := fmt.Sprintf("%s/%s", GITHUB_BASE_URL, request.YamlPath)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, GITHUB_TASK_YAML_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/artifacthub/artifacthub.go
user: login: spadgett
body: What is the use case for allowing insecure requests? We should really try to avoid this, even for development environments.

I have the same question about the other endpoints below.
created_at: 2025-02-12T20:51:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,185 @@
+package artifacthub
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+const (
+	ARTIFACTHUB_API_BASE_URL string = "https://artifacthub.io/api/v1"
+	GITHUB_BASE_URL          string = "https://github.com"
+)
+
+func GetTaskYAMLFromGithub(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request TaskYAMLRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	GITHUB_TASK_YAML_URL := fmt.Sprintf("%s/%s", GITHUB_BASE_URL, request.YamlPath)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, GITHUB_TASK_YAML_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
path: pkg/devconsole/artifacthub/artifacthub.go
user: login: spadgett
body: Are we passing the user's OpenShift access token to GitHub? Under what circumstances would we need to do that?

I have the same question about the other endpoints below.
created_at: 2025-02-12T20:53:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,185 @@
+package artifacthub
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+const (
+	ARTIFACTHUB_API_BASE_URL string = "https://artifacthub.io/api/v1"
+	GITHUB_BASE_URL          string = "https://github.com"
+)
+
+func GetTaskYAMLFromGithub(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	var request TaskYAMLRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	GITHUB_TASK_YAML_URL := fmt.Sprintf("%s/%s", GITHUB_BASE_URL, request.YamlPath)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, GITHUB_TASK_YAML_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
+	} else {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+		}
+	}
+	serviceClient := &http.Client{
+		Transport: serviceTransport,
+	}
+
+	serviceResponse, err := serviceClient.Do(serviceRequest)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to send request: %v", err)
+	}
+	defer serviceResponse.Body.Close()
+	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	return common.DevConsoleCommonResponse{
+		StatusCode: serviceResponse.StatusCode,
+		Headers:    serviceResponse.Header,
+		Body:       string(serviceResponseBody),
+	}, nil
+
+}
+
+func GetTaskDetails(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
path: pkg/devconsole/artifacthub/artifacthub.go
user: login: spadgett
body: Is the logic the same for these two additional functions as above except for building the endpoint URL? It feels like we could have more code reuse here.
created_at: 2025-02-12T20:56:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+package devconsole
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/artifacthub"
+	tektonresults "github.com/openshift/console/pkg/devconsole/tekton-results"
+	"github.com/openshift/console/pkg/devconsole/webhooks"
+	"github.com/openshift/console/pkg/serverutils"
+	"k8s.io/client-go/dynamic"
+)
+
+type handlerFunc func(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (interface{}, error)
+
+func handleRequest(w http.ResponseWriter, r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient, handler handlerFunc) {
+	response, err := handler(r, user, dynamicClient)
+	if err != nil {
+		serverutils.SendResponse(w, http.StatusInternalServerError, serverutils.ApiError{Err: err.Error()})
+		return
+	}
+	serverutils.SendResponse(w, http.StatusOK, response)
+}
+
+func Handler(user *auth.User, w http.ResponseWriter, r *http.Request, dynamicClient *dynamic.DynamicClient) {
+	path := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
+	if len(path) != 2 {
+		serverutils.SendResponse(w, http.StatusNotFound, serverutils.ApiError{Err: "Invalid URL"})
+		return
+	}
+
+	// Define routes
+	routes := map[string]map[string]handlerFunc{
path: pkg/devconsole/handler.go
user: login: spadgett
body: It feels like we should register these as separate handlers in server.go instead of handling this as a map here
created_at: 2025-02-12T21:05:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: I think this would be better as a flag that passed to bridge on startup for local dev like how we handle the metrics endpoints. For example:

https://github.com/openshift/console/blob/master/cmd/bridge/main.go#L460-L480

It would be better to avoid logic as in `isLocalhost` if we can.
created_at: 2025-02-12T21:09:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: It makes me uneasy that we're not logging `err`. Under what circumstances would we `err` not be nil? Should we be returning `err` instead of defaulting here?
created_at: 2025-02-12T21:13:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: Do we need to worry about encoding any of these path elements or are they definitely URL safe?
created_at: 2025-02-12T21:18:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, TEKTON_RESULTS_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: Same question on allowing insecure requests. What is the use case? Is this only for development?

Also for the other endpoints below.
created_at: 2025-02-12T21:18:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: Do we need to URL encode any of these parameters?
created_at: 2025-02-12T21:20:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range request.Headers {
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: Same question here on passing the user's token here. Presumably these requests are going to Github? Why would we ever pass the user's token through?
created_at: 2025-02-12T21:21:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range request.Headers {
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: Same concern about insecure requests for these endpoints
created_at: 2025-02-12T21:22:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range request.Headers {
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: We don't want to blindly pass all request headers through. At the very least, we should remove cookie and x-csrf token headers, maybe others.

Same comment for the other endpoints below.
created_at: 2025-02-12T21:24:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, TEKTON_RESULTS_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: For tekton results, we found that we require this mandatorily. 
created_at: 2025-02-19T11:54:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
path: pkg/devconsole/webhooks/webhooks.go
user: login: Lucifergene
body: Not needed here.
created_at: 2025-02-19T11:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: they are safe 
created_at: 2025-02-19T13:00:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: Currently only admin users can create the TektonResults resource.

Also the Pipelines team must have some checks for that too.
created_at: 2025-02-19T13:01:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: First, this function is completely ported from the frontend codebase to the backend. So I have not changed any logic.
Also, the details are being fetched from the TektonResults resource. Sometimes this resource may not have those fields set, but that is not an error if the resource is successfully created without those fields. 
So, if it's not there, we are just continuing with blank rather than throwing errors.
created_at: 2025-02-19T13:07:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, TEKTON_RESULTS_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: Why do we require this? Can we not use service serving certificates or another CA cert?

We should never use `InsecureSkipVerify` in a production environment.
created_at: 2025-02-19T16:40:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: Based on my reading of the `unstructured.NestedString` doc, `err` should only be returned if the value is not a string. If it's missing, `isTargetNsPresent` will be `false`. I think in that case we should handle this as an error, not ignore the error. This isn't ever expected, correct? We should at least log the problem.
created_at: 2025-02-19T16:54:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
path: pkg/devconsole/tekton-results/results.go
user: login: spadgett
body: @Lucifergene If these values are coming from the HTTP request, we can't trust them. We don't know for sure that this request is coming from console, and someone could craft parameters values that tack additional items to the query string. For example,

```
searchNamespace = "foo&some_other_param=some_other_value"
```

While if this is just a query endpoint, maybe in practice it couldn't cause damage, but it's easy to guard against. Let's properly encode the query values here and elsewhere in this PR so it's never a problem.
created_at: 2025-02-19T17:00:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,216 @@
+package webhooks
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: Let's encode the path elements anyway. It's easy to do, and it's a good policy to never trust values from the HTTP request on the backend. Then it's never a problem.
created_at: 2025-02-19T17:02:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+package webhooks
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: Can you help me understand how auth is handled here? Is this endpoint not authenticated?
created_at: 2025-02-19T17:05:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+package webhooks
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range request.Headers {
+		if key == "Cookie" || key == "X-CSRF-Token" {
+			continue
+		}
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
+
+	serviceTransport := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+	}
+
+	serviceClient := &http.Client{
+		Transport: serviceTransport,
+	}
+
+	serviceResponse, err := serviceClient.Do(serviceRequest)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to send request: %v", err)
+	}
+	defer serviceResponse.Body.Close()
+	serviceResponseBody, err := io.ReadAll(serviceResponse.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	return common.DevConsoleCommonResponse{
+		StatusCode: serviceResponse.StatusCode,
+		Headers:    serviceResponse.Header,
+		Body:       string(serviceResponseBody),
+	}, nil
+
+}
+
+func CreateGitlabWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/gitlab
+
+	var request GitlabWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GL_WEBHOOK_URL := fmt.Sprintf("%s/api/v4/projects/%s/hooks", request.HostName, request.ProjectID)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GL_WEBHOOK_URL, bytes.NewReader(bodyBytes))
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	for key, values := range request.Headers {
+		if key == "Cookie" || key == "X-CSRF-Token" {
+			continue
+		}
+		for _, value := range values {
+			serviceRequest.Header.Add(key, value)
+		}
+	}
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: I think at the very least we should move the header code into a common utility so that we're not repeating the denylist headers in each function. It's critical that the list is correct. If we ever update this list, it would be easy to update it in one place and miss the other.
created_at: 2025-02-19T17:07:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+package webhooks
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+func CreateGithubWebhook(r *http.Request, user *auth.User) (common.DevConsoleCommonResponse, error) {
+	// POST /api/dev-console/webhooks/github
+	var request GithubWebhookRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	bodyBytes, err := json.Marshal(request.Body)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to marshal request body: %v", err)
+	}
+
+	GH_WEBHOOK_URL := fmt.Sprintf("%s/repos/%s/%s/hooks", request.HostName, request.Owner, request.RepoName)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodPost, GH_WEBHOOK_URL, bytes.NewReader(bodyBytes))
path: pkg/devconsole/webhooks/webhooks.go
user: login: Lucifergene
body: Not sure if I understand the question correctly.
But if you're asking how users authenticate with GitHub to add a webhook to the repo, we send the Authorization header with a bearer token from the frontend (entered by the user). This token is included in the request body (bodyBytes). On the backend, we extract the headers, add them to our backend request, and then send it.

created_at: 2025-02-19T19:33:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: Thanks. I have logged the error
created_at: 2025-02-20T06:07:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: Thanks Sam. I got it. 
I confused encoding with something else. Its defintely a safety requirement.
created_at: 2025-02-20T06:16:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,9 @@
+export type DevConsoleEndpointRequest = {
+  allowAuthHeader?: boolean;
path: frontend/packages/console-shared/src/types/backend-api.ts
user: login: spadgett
body: Do certain endpoints always require auth and others don't? I'm just wondering if we need this in the request. It would be cleaner to allow the backend to decide whether to send the auth header only when it's needed instead of having to trust what the client says.
created_at: 2025-03-04T13:26:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+package webhooks
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"slices"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+)
+
+var headerDenyList = []string{
+	"Cookie",
+	"X-CSRF-Token",
+}
path: pkg/devconsole/webhooks/webhooks.go
user: login: spadgett
body: I'm a little hesitant to define this in two places since it's also in main.go. If we ever need to add another header to the blacklist, it would be easy to miss this spot.

https://github.com/openshift/console/blob/main/cmd/bridge/main.go#L355

Can this be passed to in as part of the `Server` config so we can define the list in one place in main.go?
created_at: 2025-03-04T13:30:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,9 @@
+export type DevConsoleEndpointRequest = {
+  allowAuthHeader?: boolean;
path: frontend/packages/console-shared/src/types/backend-api.ts
user: login: Lucifergene
body: Thanks. Removed that :)

created_at: 2025-03-05T16:48:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+package tektonresults
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+
+	"github.com/openshift/console/pkg/auth"
+	"github.com/openshift/console/pkg/devconsole/common"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var (
+	TektonResultsResource = &schema.GroupVersionResource{
+		Group:    "operator.tekton.dev",
+		Version:  "v1alpha1",
+		Resource: "tektonresults",
+	}
+	TektonResultsAPIRoute = &schema.GroupVersionResource{
+		Group:    "route.openshift.io",
+		Version:  "v1",
+		Resource: "routes",
+	}
+	cachedTektonResultsHost string = ""
+)
+
+func isLocalhost(r *http.Request) (bool, error) {
+	host := r.RemoteAddr
+	switch {
+	case host == "::1":
+		return true, nil
+	case host == "[::1]":
+		return true, nil
+	}
+	h, p, err := net.SplitHostPort(host)
+
+	// addrError helps distinguish between errors of form
+	// "no colon in address" and "too many colons in address".
+	// The former is fine as the host string need not have a
+	// port. Latter needs to be handled.
+	addrError := &net.AddrError{
+		Err:  "missing port in address",
+		Addr: host,
+	}
+	if err != nil {
+		if err.Error() != addrError.Error() {
+			return false, err
+		}
+		// host string without any port specified
+		h = host
+	} else if len(p) == 0 {
+		return false, errors.New("invalid host name format")
+	}
+
+	// use ipv4 dotted decimal for further checking
+	if h == "localhost" {
+		h = "127.0.0.1"
+	}
+	ip := net.ParseIP(h)
+
+	return ip.IsLoopback(), nil
+}
+
+func getTRHost(r *http.Request, dynamicClient *dynamic.DynamicClient) (string, error) {
+	// If running on localhost, use the route to get the host.
+	// Required for local development.
+	localHost, err := isLocalhost(r)
+	if err != nil {
+		return "", err
+	}
+	if localHost {
+		route, err := dynamicClient.Resource(*TektonResultsAPIRoute).Namespace("openshift-pipelines").Get(context.TODO(), "tekton-results-api-service", metav1.GetOptions{})
+		if err != nil {
+			return "", err
+		}
+		cachedTektonResultsHost, isHostPresent, err := unstructured.NestedString(route.Object, "spec", "host")
+		if err != nil || !isHostPresent {
+			return "", err
+		}
+		return cachedTektonResultsHost, nil
+	}
+
+	if cachedTektonResultsHost != "" {
+		return cachedTektonResultsHost, nil
+	}
+
+	host, err := dynamicClient.Resource(*TektonResultsResource).Get(context.TODO(), "result", metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+	targetNamespace, isTargetNsPresent, err := unstructured.NestedString(host.Object, "spec", "targetNamespace")
+	if err != nil || !isTargetNsPresent {
+		targetNamespace = ""
+	}
+	serverPort, isPortPresent, err := unstructured.NestedString(host.Object, "spec", "server_port")
+	if err != nil || !isPortPresent {
+		serverPort = "8080"
+	}
+	tlsHostname, isTLSHostnamePresent, err := unstructured.NestedString(host.Object, "spec", "tls_hostname_override")
+	if err != nil || !isTLSHostnamePresent {
+		tlsHostname = ""
+	}
+
+	if tlsHostname != "" {
+		cachedTektonResultsHost = fmt.Sprintf("%s:%s", tlsHostname, serverPort)
+	} else if targetNamespace != "" && serverPort != "" {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local:%s", targetNamespace, serverPort)
+	} else {
+		cachedTektonResultsHost = fmt.Sprintf("tekton-results-api-service.openshift-pipelines.svc.cluster.local:%s", serverPort)
+	}
+	return cachedTektonResultsHost, nil
+}
+
+func GetTektonResults(r *http.Request, user *auth.User, dynamicClient *dynamic.DynamicClient) (common.DevConsoleCommonResponse, error) {
+	var request TektonResultsRequest
+	err := json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to parse request: %v", err)
+	}
+
+	TEKTON_RESULTS_HOST, err := getTRHost(r, dynamicClient)
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to get TektonResults host: %v", err)
+	}
+
+	TEKTON_RESULTS_URL := fmt.Sprintf("https://%s/apis/results.tekton.dev/v1alpha2/parents/%s/results/-/records?%s", TEKTON_RESULTS_HOST, request.SearchNamespace, request.SearchParams)
+
+	var serviceRequest *http.Request
+	serviceRequest, err = http.NewRequest(http.MethodGet, TEKTON_RESULTS_URL, nil)
+
+	if err != nil {
+		return common.DevConsoleCommonResponse{}, fmt.Errorf("failed to create request: %v", err)
+	}
+
+	if request.AllowAuthHeader {
+		serviceRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token))
+	}
+
+	var serviceTransport *http.Transport
+	if request.AllowInsecure {
+		serviceTransport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		}
path: pkg/devconsole/tekton-results/results.go
user: login: Lucifergene
body: Finally, removed it. Used Certs :)
created_at: 2025-03-05T16:53:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,6 +316,9 @@ func main() {
 		srv.GOOS = runtime.GOOS
 	}
 
+	// Blacklisted headers
+	srv.DisallowedHeaderNames = []string{"Cookie", "X-CSRFToken", "X-CSRF-Token"}
path: cmd/bridge/main.go
user: login: spadgett
body: Thank you for this change.

Do we really have both `X-CSRFToken` and `X-CSRF-Token` headers?
created_at: 2025-03-05T16:56:22Z
author_association: MEMBER
diff_hunk: @@ -172,6 +172,7 @@ type Server struct {
 	AnonymousInternalProxiedK8SRT       http.RoundTripper
 	K8sMode                             string
 	K8sProxyConfig                      *proxy.Config
+	DisallowedHeaderNames               []string
path: pkg/server/server.go
user: login: spadgett
body: I would put "proxy" this name somewhere. We use these headers, but we don't want to proxy them. Maybe...?

```suggestion
	ProxyHeaderDenyList                 []string
```
created_at: 2025-03-05T16:59:43Z
author_association: MEMBER
diff_hunk: @@ -316,6 +316,9 @@ func main() {
 		srv.GOOS = runtime.GOOS
 	}
 
+	// Blacklisted headers
+	srv.DisallowedHeaderNames = []string{"Cookie", "X-CSRFToken", "X-CSRF-Token"}
path: cmd/bridge/main.go
user: login: Lucifergene
body: Actually, found both the versions when replacing. Just to be safer, added both. 

created_at: 2025-03-05T17:04:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,6 +316,9 @@ func main() {
 		srv.GOOS = runtime.GOOS
 	}
 
+	// Blacklisted headers
+	srv.DisallowedHeaderNames = []string{"Cookie", "X-CSRFToken", "X-CSRF-Token"}
path: cmd/bridge/main.go
user: login: spadgett
body: Hmm, I don't see it. Is my search wrong?

If there are two, we should fix the `X-CSRF-Token` reference.

```
❯ rg X-CSRF
cmd/bridge/main.go
355:                    HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
386:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
391:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
396:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
402:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
407:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
412:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
420:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
446:                    HeaderBlacklist:         []string{"Cookie", "X-CSRFToken"},
467:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
472:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
477:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
489:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
494:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
499:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
513:                            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},
533:            HeaderBlacklist: []string{"Cookie", "X-CSRFToken"},

pkg/auth/csrfverifier/csrf.go
16:     CSRFHeader     = "X-CSRFToken"

pkg/proxy/proxy.go
102:var HeaderBlacklist = []string{"Cookie", "X-CSRFToken"}
105:// except "Cookie" and "X-CSRFToken" headers.

frontend/packages/console-dynamic-plugin-sdk/src/utils/fetch/console-fetch-utils.ts
7:  'X-CSRFToken'?: string;
34:    'X-CSRFToken': getCSRFToken(),

frontend/public/co-fetch.ts
9:    options.headers['X-CSRFToken'] = token;
11:    options.headers = { 'X-CSRFToken': token };
14:  // X-CSRFToken is used only for non-GET requests targeting bridge
16:    delete options.headers['X-CSRFToken'];
```
created_at: 2025-03-05T18:07:25Z
author_association: MEMBER
diff_hunk: @@ -316,6 +316,9 @@ func main() {
 		srv.GOOS = runtime.GOOS
 	}
 
+	// Blacklisted headers
+	srv.DisallowedHeaderNames = []string{"Cookie", "X-CSRFToken", "X-CSRF-Token"}
path: cmd/bridge/main.go
user: login: spadgett
body: @Lucifergene Adding the extra value here wouldn't cause a problem, but if something is using the wrong header name that's an existing bug. `X-CSRF-Token` won't work. It would be good to track this down.
created_at: 2025-03-11T14:17:53Z
author_association: MEMBER

PR comments:
user: login: openshift-ci-robot
created_at: 2024-10-24T10:02:03Z
body: @Lucifergene: An error was encountered searching for bug OCPBUGS-36652 on the Jira server at https://issues.redhat.com/. No known errors were detected, please see the full error message for details.

<details><summary>Full error message.</summary>

<code>
You do not have the permission to see the specified issue.: request failed. Please analyze the request body for more details. Status code: 403: 
</code>

</details>

Please contact an administrator to resolve this issue, then request a bug refresh with <code>/jira refresh</code>.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2024-10-24T10:03:44Z
body: @Lucifergene: An error was encountered searching for bug OCPBUGS-36652 on the Jira server at https://issues.redhat.com/. No known errors were detected, please see the full error message for details.

<details><summary>Full error message.</summary>

<code>
You do not have the permission to see the specified issue.: request failed. Please analyze the request body for more details. Status code: 403: 
</code>

</details>

Please contact an administrator to resolve this issue, then request a bug refresh with <code>/jira refresh</code>.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>This pull request introduces SSRF (Server-Side Request Forgery) protection in the `pkg/devconsole/proxy` package. The most important changes include adding a URL validation function, updating the `serve` function to use this validation, and adding tests for the new functionality.
>
>### SSRF Protection Implementation:
>* [`pkg/devconsole/proxy/proxy.go`](diffhunk://#diff-e6afaef81a1d41c4545ced85d8389ed750ead55051b4f1ea6639f384b11cb106R45-R49): Added URL validation to the `serve` function to prevent SSRF attacks.
>* [`pkg/devconsole/proxy/ssrf_utils.go`](diffhunk://#diff-6ba40febbee4e17b3b1ef0dd55add6394b6d65a04330fdb5ad24310696316c86R1-R137): Implemented the `validateRequestURL` function and supporting utilities to check for private IPs, resolve hostnames, and ensure URLs are safe.
>
>### Code Quality Improvements:
>* [`pkg/devconsole/proxy/proxy.go`](diffhunk://#diff-e6afaef81a1d41c4545ced85d8389ed750ead55051b4f1ea6639f384b11cb106L97-R107): Standardized error message formatting for consistency.
>
>### Testing:
>* [`pkg/devconsole/proxy/ssrf_test.go`](diffhunk://#diff-3a0046e4a5a780024f2b6283ee22c6380abbeb3025f01806da80589cf7700b7bR1-R55): Added a comprehensive set of tests for the `validateRequestURL` function to ensure it correctly identifies valid and invalid URLs.


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: Lucifergene
created_at: 2024-10-24T10:11:41Z
body: /cc @vikram-raj @lokanandaprabhu 
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2024-10-24T10:11:47Z
body: @Lucifergene: GitHub didn't allow me to request PR reviews from the following users: christoph-jerolimov.

Note that only [openshift members](https://github.com/orgs/openshift/people) and repo collaborators can review this PR, and authors cannot review their own PRs.

<details>

In response to [this](https://github.com/openshift/console/pull/14115#issuecomment-2434861231):

>/cc @christoph-jerolimov @vikram-raj @lokanandaprabhu 


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2024-10-24T13:10:48Z
body: @Lucifergene: An error was encountered searching for bug OCPBUGS-36652 on the Jira server at https://issues.redhat.com/. No known errors were detected, please see the full error message for details.

<details><summary>Full error message.</summary>

<code>
You do not have the permission to see the specified issue.: request failed. Please analyze the request body for more details. Status code: 403: 
</code>

</details>

Please contact an administrator to resolve this issue, then request a bug refresh with <code>/jira refresh</code>.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>This pull request introduces significant improvements to the SSRF (Server-Side Request Forgery) protection in the `pkg/devconsole/proxy` package. The changes include adding a validation function for request URLs, updating error messages for consistency, and enhancing the test coverage for the new validation logic.
>
>### SSRF Protection Enhancements:
>
>* [`pkg/devconsole/proxy/proxy.go`](diffhunk://#diff-e6afaef81a1d41c4545ced85d8389ed750ead55051b4f1ea6639f384b11cb106R45-R49): Added a new validation step to check the request URL for SSRF vulnerabilities before processing the request.
>
>### Code Consistency:
>
>* [`pkg/devconsole/proxy/proxy.go`](diffhunk://#diff-e6afaef81a1d41c4545ced85d8389ed750ead55051b4f1ea6639f384b11cb106L97-R107): Updated error messages to use consistent capitalization for better readability.
>
>### Test Coverage Improvements:
>
>* [`pkg/devconsole/proxy/ssrf_test.go`](diffhunk://#diff-3a0046e4a5a780024f2b6283ee22c6380abbeb3025f01806da80589cf7700b7bR1-R55): Introduced new test cases to verify the behavior of the `validateRequestURL` function, covering various valid and invalid URL scenarios.
>* [`pkg/devconsole/proxy/proxy_test.go`](diffhunk://#diff-78dc92931f07d7790c90f334c28de34f08153c49800e42723caf5e2ca1c53765L29-R28): Updated existing test cases to use a real URL and replaced the mocked response body with a realistic API response. [[1]](diffhunk://#diff-78dc92931f07d7790c90f334c28de34f08153c49800e42723caf5e2ca1c53765L29-R28) [[2]](diffhunk://#diff-78dc92931f07d7790c90f334c28de34f08153c49800e42723caf5e2ca1c53765L39-R44) [[3]](diffhunk://#diff-78dc92931f07d7790c90f334c28de34f08153c49800e42723caf5e2ca1c53765L54-L70) [[4]](diffhunk://#diff-78dc92931f07d7790c90f334c28de34f08153c49800e42723caf5e2ca1c53765L86-R83)
>
>### Utility Functions:
>
>* [`pkg/devconsole/proxy/ssrf_utils.go`](diffhunk://#diff-6ba40febbee4e17b3b1ef0dd55add6394b6d65a04330fdb5ad24310696316c86R1-R137): Added utility functions for URL validation, including checks for allowed URLs, private IPs, public IPs, and DNS rebinding protection.


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: Lucifergene
created_at: 2024-10-28T10:47:39Z
body: /label tide/merge-method-squash
author_association: CONTRIBUTOR
user: login: Lucifergene
created_at: 2024-10-28T11:40:41Z
body: /cc @christoph-jerolimov 
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-01-28T09:05:40Z
body: @Lucifergene: This pull request references [Jira Issue OCPBUGS-36652](https://issues.redhat.com//browse/OCPBUGS-36652), which is invalid:
 - expected the bug to target either version "4.19.*" or "openshift-4.19.*", but it targets "4.17.z" instead

Comment <code>/jira refresh</code> to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>Jira Task: https://issues.redhat.com/browse/ODC-7764
>
>Removed the common devconsole proxy.
>
>Created 6 new endpoints:
>
>Artifacthub
>POST /api/dev-console/artifacthub/search
>POST /api/dev-console/artifacthub/get
>POST /api/dev-console/artifacthub/yaml
>Tekton-results
>POST /api/dev-console/tekton-results/get
>POST /api/dev-console/tekton-results/logs
>POST /api/dev-console/tekton-results/summary


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-01-28T09:06:27Z
body: @Lucifergene: This pull request references [ODC-7764](https://issues.redhat.com//browse/ODC-7764) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "4.19.0" version, but no target version was set.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>Jira Task: https://issues.redhat.com/browse/ODC-7764
>
>Removed the common devconsole proxy.
>
>Created 6 new endpoints:
>
>1. Artifacthub
>
>- POST /api/dev-console/artifacthub/search
>- POST /api/dev-console/artifacthub/get
>- POST /api/dev-console/artifacthub/yaml
>
>2. Tekton-results
>
>- POST /api/dev-console/tekton-results/get
>- POST /api/dev-console/tekton-results/logs
>- POST /api/dev-console/tekton-results/summary


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-01-30T13:10:09Z
body: /retest
author_association: MEMBER
user: login: vikram-raj
created_at: 2025-02-03T15:53:22Z
body: @Lucifergene, the Tekton results endpoint `/API/dev-console/tekton-results/get` is failing with a 500 error. It looks like you are looking for the route, but in a cluster, the request should go through the service instead of the route. 

Check out how we were creating the endpoint for the service 

https://github.com/openshift-pipelines/console-plugin/blob/f456a5fa884c04ba11355ece3eabdc20f9bacffa/src/components/utils/tekton-results.ts#L239


<img width="924" alt="image" src="https://github.com/user-attachments/assets/1c2b3f60-6874-4a2e-8c74-d0fd216f9f42" />

author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-02-03T16:06:45Z
body: @Lucifergene: This pull request references [ODC-7764](https://issues.redhat.com//browse/ODC-7764) which is a valid jira issue.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>Jira Task: https://issues.redhat.com/browse/ODC-7764
>
>Removed the common devconsole proxy.
>
>Created 6 new endpoints:
>
>1. Artifacthub
>
>- POST /api/dev-console/artifacthub/search
>- POST /api/dev-console/artifacthub/get
>- POST /api/dev-console/artifacthub/yaml
>
>2. Tekton-results
>
>- POST /api/dev-console/tekton-results/get
>- POST /api/dev-console/tekton-results/logs
>- POST /api/dev-console/tekton-results/summary
>
>3. Webhooks
>
>- POST /api/dev-console/webhooks/github
>- POST /api/dev-console/webhooks/gitlab
>- POST /api/dev-console/webhooks/bitbucket


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-02-11T14:38:16Z
body: /retest
author_association: MEMBER
user: login: vikram-raj
created_at: 2025-02-11T14:39:02Z
body: /retitle OCPBUGS-36652: Remove the Backend Common Proxy and Replace it with Dedicated ones
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-02-11T14:41:40Z
body: @Lucifergene: This pull request references [Jira Issue OCPBUGS-36652](https://issues.redhat.com//browse/OCPBUGS-36652), which is invalid:
 - expected the bug to target either version "4.19.*" or "openshift-4.19.*", but it targets "4.17.z" instead

Comment <code>/jira refresh</code> to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>Jira Task: https://issues.redhat.com/browse/ODC-7764
>
>Removed the common devconsole proxy.
>
>Created 6 new endpoints:
>
>1. Artifacthub
>
>- POST /api/dev-console/artifacthub/search
>- POST /api/dev-console/artifacthub/get
>- POST /api/dev-console/artifacthub/yaml
>
>2. Tekton-results
>
>- POST /api/dev-console/tekton-results/get
>- POST /api/dev-console/tekton-results/logs
>- POST /api/dev-console/tekton-results/summary
>
>3. Webhooks
>
>- POST /api/dev-console/webhooks/github
>- POST /api/dev-console/webhooks/gitlab
>- POST /api/dev-console/webhooks/bitbucket


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-02-11T14:44:06Z
body: /jira refresh
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-02-11T14:44:17Z
body: @vikram-raj: This pull request references [Jira Issue OCPBUGS-36652](https://issues.redhat.com//browse/OCPBUGS-36652), which is valid.

<details><summary>3 validation(s) were run on this bug</summary>

* bug is open, matching expected state (open)
* bug target version (4.19.0) matches configured target version for branch (4.19.0)
* bug is in the state POST, which is one of the valid states (NEW, ASSIGNED, POST)</details>

Requesting review from QA contact:
/cc @yapei

<details>

In response to [this](https://github.com/openshift/console/pull/14115#issuecomment-2651029774):

>/jira refresh


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-03-04T12:20:38Z
body: /retest
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-03-11T06:48:28Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/console/pull/14115#pullrequestreview-2673040650" title="LGTM">lokanandaprabhu</a>*, *<a href="https://github.com/openshift/console/pull/14115#" title="Author self-approved">Lucifergene</a>*, *<a href="https://github.com/openshift/console/pull/14115#pullrequestreview-2661783896" title="Approved">spadgett</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fconsole).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/OWNERS](https://github.com/openshift/console/blob/main/cmd/OWNERS)~~ [spadgett]
- ~~[frontend/OWNERS](https://github.com/openshift/console/blob/main/frontend/OWNERS)~~ [spadgett]
- ~~[pkg/OWNERS](https://github.com/openshift/console/blob/main/pkg/OWNERS)~~ [spadgett]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-03-11T07:07:21Z
body: /unhold
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-03-11T11:59:33Z
body: /retest-required

Remaining retests: 0 against base HEAD cd08e7307ab337e85bdb58a9d984aa5b47103d59 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-03-11T12:16:27Z
body: /retest
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-03-11T14:12:15Z
body: /retest-required

Remaining retests: 0 against base HEAD cd08e7307ab337e85bdb58a9d984aa5b47103d59 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: invincibleJai
created_at: 2025-03-12T05:09:48Z
body: /retest
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-03-12T12:11:18Z
body: /retest-required

Remaining retests: 0 against base HEAD cd08e7307ab337e85bdb58a9d984aa5b47103d59 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-12T17:15:11Z
body: /retest-required

Remaining retests: 0 against base HEAD 93605f1c7063a5f48c72f8610509435892d9f659 and 1 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-12T21:38:12Z
body: /retest-required

Remaining retests: 0 against base HEAD 93605f1c7063a5f48c72f8610509435892d9f659 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-13T02:41:23Z
body: /retest-required

Remaining retests: 0 against base HEAD 7a9c456b332fe0f3298604c1215ca8c83e9fdeaf and 1 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-13T11:41:30Z
body: /retest-required

Remaining retests: 0 against base HEAD 57a4cdec7c9b5c24481efc279794f2208cc25c74 and 0 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-13T16:46:22Z
body: /retest-required

Remaining retests: 0 against base HEAD 57a4cdec7c9b5c24481efc279794f2208cc25c74 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-13T18:43:21Z
body: /retest-required

Remaining retests: 0 against base HEAD 57a4cdec7c9b5c24481efc279794f2208cc25c74 and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-13T23:30:01Z
body: /retest-required

Remaining retests: 0 against base HEAD 0ed47054ebea0ed05ec8f2b3c1d60d130bbd42fa and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-14T04:03:41Z
body: /retest-required

Remaining retests: 0 against base HEAD 6c2219f4516ef236bb523398c085942e55a1afcd and 2 for PR HEAD a997cb05e872fc440c2aba0b5e856c785e105520 in total

author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-03-14T13:13:16Z
body: @Lucifergene: all tests **passed!**


[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=console&pr=14115). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:Lucifergene).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-14T13:17:25Z
body: @Lucifergene: [Jira Issue OCPBUGS-36652](https://issues.redhat.com//browse/OCPBUGS-36652): All pull requests linked via external trackers have merged:
 * [openshift/console#14115](https://github.com/openshift/console/pull/14115)

[Jira Issue OCPBUGS-36652](https://issues.redhat.com//browse/OCPBUGS-36652) has been moved to the MODIFIED state.

<details>

In response to [this](https://github.com/openshift/console/pull/14115):

>Jira Task: https://issues.redhat.com/browse/ODC-7764
>
>Removed the common devconsole proxy.
>
>Created 6 new endpoints:
>
>1. Artifacthub
>
>- POST /api/dev-console/artifacthub/search
>- POST /api/dev-console/artifacthub/get
>- POST /api/dev-console/artifacthub/yaml
>
>2. Tekton-results
>
>- POST /api/dev-console/tekton-results/get
>- POST /api/dev-console/tekton-results/logs
>- POST /api/dev-console/tekton-results/summary
>
>3. Webhooks
>
>- POST /api/dev-console/webhooks/github
>- POST /api/dev-console/webhooks/gitlab
>- POST /api/dev-console/webhooks/bitbucket


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-bot
created_at: 2025-03-14T19:50:40Z
body: **[ART PR BUILD NOTIFIER]**

Distgit: openshift-enterprise-console
This PR has been included in build [openshift-enterprise-console-container-v4.19.0-202503141716.p0.g3ef4644.assembly.stream.el9](https://brewweb.engineering.redhat.com/brew/buildinfo?buildID=3557505).
All builds following this will include this PR.
author_association: CONTRIBUTOR
user: login: vikram-raj
created_at: 2025-05-09T10:59:56Z
body: /cherry-pick release-4.18
author_association: MEMBER
user: login: openshift-cherrypick-robot
created_at: 2025-05-09T11:00:39Z
body: @vikram-raj: new pull request created: #15042

<details>

In response to [this](https://github.com/openshift/console/pull/14115#issuecomment-2866122152):

>/cherry-pick release-4.18


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE