Repository: Azure/ARO-RP
Pull request #2491 (2022-10-27 05:05:50)

Add npm audit GitHub Action for portal/v2
### Which issue this PR addresses:
[ADO CARD](https://msazure.visualstudio.com/AzureRedHatOpenShift/_workitems/edit/16018120)

### What this PR does / why we need it:
Adds `npm audit` to portal v2 at minimal cost to time (~20s) or process per pull request to ensure that we are ALWAYS aware if our production facing admin portal contains vulnerabilities.

### Test plan for issue:
Created PR and tested GitHub Action.

### Is there any documentation that needs to be updated for this PR?
No. Standard internal CI/CD implementation.



Patch:
diff --git a/.github/workflows/npm-audit.yml b/.github/workflows/npm-audit.yml
new file mode 100644
index 000000000..573962567
--- /dev/null
+++ b/.github/workflows/npm-audit.yml
@@ -0,0 +1,25 @@
+name: node-lint
+on:
+  push:
+    tags:
+      - v*
+    branches:
+      - master
+      - main
+  pull_request:
+permissions:
+  contents: read
+jobs:
+  npm-audit-portal-v2:
+    name: npm-audit-portal-v2
+    runs-on: ubuntu-latest
+    steps:
+      - name: setup Node.JS
+        uses: actions/setup-node@v3
+        with:
+          node-version: 16.16.0
+      - name: Checkout Code
+        uses: actions/checkout@v3
+      - name: Run NPM Audit
+        run: bash ${GITHUB_WORKSPACE}/hack/github-actions/npm_audit.sh
+        shell: bash
diff --git a/hack/github-actions/npm_audit.sh b/hack/github-actions/npm_audit.sh
new file mode 100644
index 000000000..b3e8b6091
--- /dev/null
+++ b/hack/github-actions/npm_audit.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+cd ./portal/v2
+
+if npm audit --omit=dev | grep -q 'found 0 vulnerabilities'; then
+  echo "No vulnerabilities found."
+  exit 0
+else
+  echo "Vulnerabilities found, please run `npm audit --omit=dev` locally and fix or record issues and create an ADO card."
+  exit 1
+fi


Test patch:
diff --git a/test/e2e/admin_portal.go b/test/e2e/admin_portal.go
index 17f14b387..c3e278e83 100644
--- a/test/e2e/admin_portal.go
+++ b/test/e2e/admin_portal.go
@@ -7,16 +7,22 @@ import (
 	"os"
 	"time"
 
-	. "github.com/onsi/ginkgo/v2"                        //nolint
-	. "github.com/onsi/gomega"                           //nolint
-	. "github.com/serge1peshcoff/selenium-go-conditions" //nolint
-	. "github.com/tebeka/selenium"                       //nolint
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	conditions "github.com/serge1peshcoff/selenium-go-conditions"
+	"github.com/tebeka/selenium"
 )
 
 var _ = Describe("Admin Portal E2E Testing", func() {
-	BeforeEach(skipIfNotInDevelopmentEnv)
-	var wdPoint *WebDriver
-	var wd WebDriver
+	BeforeEach(
+		func() {
+			skipIfNotInDevelopmentEnv()
+			skipIfDockerNotWorking()
+		},
+	)
+	var wdPoint *selenium.WebDriver
+	var wd selenium.WebDriver
 	var host string
 
 	JustBeforeEach(func() {
@@ -33,12 +39,12 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 	})
 
 	It("Should be able to populate cluster data correctly", func() {
-		err := wd.Wait(ElementIsLocated(ByCSSSelector, "div[data-automation-key='name']"))
+		err := wd.Wait(conditions.ElementIsLocated(selenium.ByCSSSelector, "div[data-automation-key='name']"))
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
-		cluster, err := wd.FindElement(ByCSSSelector, "div[data-automation-key='name']")
+		cluster, err := wd.FindElement(selenium.ByCSSSelector, "div[data-automation-key='name']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -47,9 +53,9 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 	})
 
 	It("Should be able to filter cluster data correctly", func() {
-		wd.Wait(ElementIsLocated(ByCSSSelector, "div[data-automation-key='name']"))
+		wd.Wait(conditions.ElementIsLocated(selenium.ByCSSSelector, "div[data-automation-key='name']"))
 
-		filter, err := wd.FindElement(ByCSSSelector, "input[placeholder='Filter on resource ID']")
+		filter, err := wd.FindElement(selenium.ByCSSSelector, "input[placeholder='Filter on resource ID']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -57,8 +63,8 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 		// Set filter so it doesn't match cluster name
 		filter.SendKeys("Incorrect Cluster")
 
-		wd.Wait(ElementIsLocated(ByID, "ClusterCount"))
-		text, err := wd.FindElement(ByID, "ClusterCount")
+		wd.Wait(conditions.ElementIsLocated(selenium.ByID, "ClusterCount"))
+		text, err := wd.FindElement(selenium.ByID, "ClusterCount")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -69,12 +75,12 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 	It("Should be able to populate cluster info panel correctly", func() {
 		const CLUSTER_INFO_HEADINGS = 17
 
-		err := wd.Wait(ElementIsLocated(ByCSSSelector, "div[data-automation-key='name']"))
+		err := wd.Wait(conditions.ElementIsLocated(selenium.ByCSSSelector, "div[data-automation-key='name']"))
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
-		cluster, err := wd.FindElement(ByCSSSelector, "div[data-automation-key='name']")
+		cluster, err := wd.FindElement(selenium.ByCSSSelector, "div[data-automation-key='name']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -84,12 +90,12 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 			SaveScreenshotAndExit(wd, err)
 		}
 
-		err = wd.WaitWithTimeout(ElementIsLocated(ByID, "ClusterDetailCell"), 2*time.Minute)
+		err = wd.WaitWithTimeout(conditions.ElementIsLocated(selenium.ByID, "ClusterDetailCell"), 2*time.Minute)
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
-		panelSpans, err := wd.FindElements(ByID, "ClusterDetailCell")
+		panelSpans, err := wd.FindElements(selenium.ByID, "ClusterDetailCell")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -129,23 +135,23 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 	})
 
 	It("Should be able to copy cluster resource id", func() {
-		wd.Wait(ElementIsLocated(ByCSSSelector, "button[aria-label='Copy Resource ID']"))
+		wd.Wait(conditions.ElementIsLocated(selenium.ByCSSSelector, "button[aria-label='Copy Resource ID']"))
 
-		button, err := wd.FindElement(ByCSSSelector, "button[aria-label='Copy Resource ID']")
+		button, err := wd.FindElement(selenium.ByCSSSelector, "button[aria-label='Copy Resource ID']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
 		button.Click()
 
-		filter, err := wd.FindElement(ByCSSSelector, "input[placeholder='Filter on resource ID']")
+		filter, err := wd.FindElement(selenium.ByCSSSelector, "input[placeholder='Filter on resource ID']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
 		// Paste clipboard
 		filter.Click()
-		filter.SendKeys(ControlKey + "v")
+		filter.SendKeys(selenium.ControlKey + "v")
 		resourceId, err := filter.GetAttribute("value")
 
 		if err != nil {
@@ -156,35 +162,35 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 	})
 
 	It("Should be able to open ssh panel and get ssh details", func() {
-		wd.Wait(ElementIsLocated(ByCSSSelector, "button[aria-label='SSH']"))
+		wd.Wait(conditions.ElementIsLocated(selenium.ByCSSSelector, "button[aria-label='SSH']"))
 
-		button, err := wd.FindElement(ByCSSSelector, "button[aria-label='SSH']")
+		button, err := wd.FindElement(selenium.ByCSSSelector, "button[aria-label='SSH']")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
 		button.Click()
 
-		wd.Wait(ElementIsLocated(ByID, "sshModal"))
-		wd.Wait(ElementIsLocated(ByID, "sshDropdown"))
+		wd.Wait(conditions.ElementIsLocated(selenium.ByID, "sshModal"))
+		wd.Wait(conditions.ElementIsLocated(selenium.ByID, "sshDropdown"))
 
-		sshDropdown, err := wd.FindElement(ByID, "sshDropdown")
+		sshDropdown, err := wd.FindElement(selenium.ByID, "sshDropdown")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
 		sshDropdown.Click()
 
-		wd.Wait(ElementIsLocated(ByID, "sshDropdown-list0"))
-		machine, err := wd.FindElement(ByID, "sshDropdown-list0")
+		wd.Wait(conditions.ElementIsLocated(selenium.ByID, "sshDropdown-list0"))
+		machine, err := wd.FindElement(selenium.ByID, "sshDropdown-list0")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
 
 		machine.Click()
 
-		wd.Wait(ElementIsLocated(ByID, "sshButton"))
-		requestBtn, err := wd.FindElement(ByID, "sshButton")
+		wd.Wait(conditions.ElementIsLocated(selenium.ByID, "sshButton"))
+		requestBtn, err := wd.FindElement(selenium.ByID, "sshButton")
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
@@ -192,7 +198,7 @@ var _ = Describe("Admin Portal E2E Testing", func() {
 		requestBtn.Click()
 
 		// Test fails if these labels aren't present
-		err = wd.Wait(ElementIsLocated(ByID, "sshCommand"))
+		err = wd.Wait(conditions.ElementIsLocated(selenium.ByID, "sshCommand"))
 		if err != nil {
 			SaveScreenshotAndExit(wd, err)
 		}
diff --git a/test/e2e/setup.go b/test/e2e/setup.go
index e3b2865c5..7ed299fc1 100644
--- a/test/e2e/setup.go
+++ b/test/e2e/setup.go
@@ -17,9 +17,8 @@ import (
 	"strings"
 	"time"
 
-	. "github.com/onsi/ginkgo/v2"  //nolint
-	. "github.com/onsi/gomega"     //nolint
-	. "github.com/tebeka/selenium" //nolint
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 
 	"github.com/Azure/go-autorest/autorest/azure/auth"
 	configclient "github.com/openshift/client-go/config/clientset/versioned"
@@ -27,6 +26,7 @@ import (
 	projectclient "github.com/openshift/client-go/project/clientset/versioned"
 	mcoclient "github.com/openshift/machine-config-operator/pkg/generated/clientset/versioned"
 	"github.com/sirupsen/logrus"
+	"github.com/tebeka/selenium"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
@@ -75,15 +75,25 @@ var (
 	vnetResourceGroup string
 	clusterName       string
 	clients           *clientSet
+
+	dockerSucceeded bool
 )
 
 func skipIfNotInDevelopmentEnv() {
 	if !_env.IsLocalDevelopmentMode() {
-		Skip("skipping tests in non-development environment")
+		Skip("skipping portal tests in non-development environment")
+	}
+}
+
+func skipIfDockerNotWorking() {
+	// docker cmds will fail in INT until we figure out a solution since
+	// it is running from docker already
+	if !dockerSucceeded {
+		Skip("skipping portal tests as docker is not available")
 	}
 }
 
-func SaveScreenshotAndExit(wd WebDriver, e error) {
+func SaveScreenshotAndExit(wd selenium.WebDriver, e error) {
 	log.Infof("Error : %s", e.Error())
 	log.Info("Taking Screenshot and saving page source")
 	imageBytes, err := wd.Screenshot()
@@ -151,7 +161,7 @@ func SaveScreenshotAndExit(wd WebDriver, e error) {
 	panic(e)
 }
 
-func adminPortalSessionSetup() (string, *WebDriver) {
+func adminPortalSessionSetup() (string, *selenium.WebDriver) {
 	const (
 		hubPort  = 4444
 		hostPort = 8444
@@ -159,11 +169,11 @@ func adminPortalSessionSetup() (string, *WebDriver) {
 
 	os.Setenv("SE_SESSION_REQUEST_TIMEOUT", "9000")
 
-	caps := Capabilities{
+	caps := selenium.Capabilities{
 		"browserName":         "MicrosoftEdge",
 		"acceptInsecureCerts": true,
 	}
-	wd := WebDriver(nil)
+	wd := selenium.WebDriver(nil)
 
 	_, err := url.ParseRequestURI(fmt.Sprintf("https://localhost:%d", hubPort))
 	if err != nil {
@@ -171,7 +181,7 @@ func adminPortalSessionSetup() (string, *WebDriver) {
 	}
 
 	for i := 0; i < 10; i++ {
-		wd, err = NewRemote(caps, fmt.Sprintf("http://localhost:%d/wd/hub", hubPort))
+		wd, err = selenium.NewRemote(caps, fmt.Sprintf("http://localhost:%d/wd/hub", hubPort))
 		if wd != nil {
 			err = nil
 			break
@@ -207,7 +217,7 @@ func adminPortalSessionSetup() (string, *WebDriver) {
 
 	log.Infof("Session Output : %s\n", os.Getenv("SESSION"))
 
-	cookie := &Cookie{
+	cookie := &selenium.Cookie{
 		Name:   "session",
 		Value:  os.Getenv("SESSION"),
 		Expiry: math.MaxUint32,
@@ -307,7 +317,8 @@ func setupSelenium(ctx context.Context) error {
 	cmd := exec.CommandContext(ctx, "docker", "pull", "selenium/standalone-edge:latest")
 	output, err := cmd.CombinedOutput()
 	if err != nil {
-		log.Fatalf("Error occurred pulling selenium image\n Output: %s\n Error: %s\n", output, err)
+		log.Printf("Error occurred pulling selenium image\n Output: %s\n Error: %s\n", output, err)
+		dockerSucceeded = false
 	}
 
 	log.Infof("Selenium Image Pull Output : %s\n", output)
@@ -315,7 +326,8 @@ func setupSelenium(ctx context.Context) error {
 	cmd = exec.CommandContext(ctx, "docker", "run", "-d", "-p", "4444:4444", "--name", seleniumContainerName, "--network=host", "--shm-size=2g", "selenium/standalone-edge:latest")
 	output, err = cmd.CombinedOutput()
 	if err != nil {
-		log.Fatalf("Error occurred starting selenium grid\n Output: %s\n Error: %s\n", output, err)
+		log.Printf("Error occurred starting selenium grid\n Output: %s\n Error: %s\n", output, err)
+		dockerSucceeded = false
 	}
 
 	log.Infof("Selenium Container Run Output : %s\n", output)
@@ -328,14 +340,18 @@ func tearDownSelenium(ctx context.Context) error {
 	cmd := exec.CommandContext(ctx, "docker", "stop", seleniumContainerName)
 	output, err := cmd.CombinedOutput()
 	if err != nil {
-		log.Fatalf("Error occurred stopping selenium container\n Output: %s\n Error: %s\n", output, err)
+		log.Printf("Error occurred stopping selenium container\n Output: %s\n Error: %s\n", output, err)
+		dockerSucceeded = false
+		return err
 	}
 
 	log.Infof("Removing Selenium Grid container")
 	cmd = exec.CommandContext(ctx, "docker", "rm", seleniumContainerName)
 	output, err = cmd.CombinedOutput()
 	if err != nil {
-		log.Fatalf("Error occurred removing selenium grid container\n Output: %s\n Error: %s\n", output, err)
+		log.Printf("Error occurred removing selenium grid container\n Output: %s\n Error: %s\n", output, err)
+		dockerSucceeded = false
+		return err
 	}
 
 	return nil
@@ -408,6 +424,6 @@ var _ = AfterSuite(func() {
 	log.Info("AfterSuite")
 
 	if err := tearDown(context.Background()); err != nil {
-		panic(err)
+		log.Printf(err.Error())
 	}
 })