Skip to content

Validation statuses polling

What does this MR do?

This introduces live validation status updates to the DAST site profiles library.

The logic is as follows:

  1. When the library loads, any profile that is currently being validated has its normalized URL[1] added to a stack.
  2. If the user manually triggers validation on any non-validated profile, that profile's normalized URL is added the above-mentioned stack.
  3. Whenever the normalized URLs stack contains item, a GraphQL smart query starts fetching up-to-date statuses and overwirites the data in the local cache to keep the list up-to-date in the UI.
  4. Once a validation ends (because it either failed or succeeded), the corresponding normalized URL is removed from the stack.

[1]: note that normalized URLs aren't supported yet, so the MR currently mocks the behavior by using the profiles' target URL.

How to test this?

  1. Enable the feature flag:
echo "Feature.enable(:security_on_demand_scans_site_validation)" | rails c
  1. Since the backend is currently not ready to support this feature, apply the following patch to mock the GraphQL queries client-side. This will mock the validations query by returning some random statuses, and the targetUrl will be used instead of normalizedTargetUrl which doesn't exist yet.
Patch
diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/components/dast_site_profiles_list.vue b/ee/app/assets/javascripts/security_configuration/dast_profiles/components/dast_site_profiles_list.vue
index aa60f7f90b6..2eeb7d987a7 100644
--- a/ee/app/assets/javascripts/security_configuration/dast_profiles/components/dast_site_profiles_list.vue
+++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/components/dast_site_profiles_list.vue
@@ -88,12 +88,12 @@ export default {
         if (!this.glFeatures.securityOnDemandScansSiteValidation) {
           return;
         }
-        profiles.forEach(({ validationStatus, normalizedTargetUrl }) => {
+        profiles.forEach(({ validationStatus, targetUrl }) => {
           if (
             [PENDING, INPROGRESS].includes(validationStatus) &&
-            !this.urlsPendingValidation.includes(normalizedTargetUrl)
+            !this.urlsPendingValidation.includes(targetUrl)
           ) {
-            this.urlsPendingValidation.push(normalizedTargetUrl);
+            this.urlsPendingValidation.push(targetUrl);
           }
         });
       },
@@ -118,10 +118,10 @@ export default {
         this.showValidationModal();
       });
     },
-    startValidatingProfile({ normalizedTargetUrl }) {
+    startValidatingProfile({ targetUrl }) {
       updateSiteProfilesStatuses({
         fullPath: this.fullPath,
-        normalizedTargetUrl,
+        normalizedTargetUrl: targetUrl,
         status: PENDING,
         store: this.$apolloProvider.defaultClient,
       });
diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/cache_utils.js b/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/cache_utils.js
index 18037adc230..5eabcf981a3 100644
--- a/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/cache_utils.js
+++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/cache_utils.js
@@ -69,7 +69,7 @@ export const updateSiteProfilesStatuses = ({ fullPath, normalizedTargetUrl, stat
   const sourceData = store.readQuery(queryBody);
 
   const profilesWithNormalizedTargetUrl = sourceData.project.siteProfiles.edges.flatMap(
-    ({ node }) => (node.normalizedTargetUrl === normalizedTargetUrl ? node : []),
+    ({ node }) => (node.targetUrl === normalizedTargetUrl ? node : []),
   );
 
   profilesWithNormalizedTargetUrl.forEach(({ id }) => {
diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/provider.js b/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/provider.js
index 80e16fe53a1..4f56820b1cc 100644
--- a/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/provider.js
+++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/provider.js
@@ -1,14 +1,36 @@
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
+import { random } from 'lodash';
 import createDefaultClient from '~/lib/graphql';
 
 Vue.use(VueApollo);
 
-export default new VueApollo({
-  defaultClient: createDefaultClient(
-    {},
-    {
-      assumeImmutableResults: true,
+const resolvers = {
+  Query: {
+    dastSiteValidations: (_, { normalizedTargetUrls }) => {
+      return {
+        nodes: normalizedTargetUrls.map(url => {
+          const randNumber = random(100);
+          let validationStatus = 'INPROGRESS_VALIDATION';
+          if (randNumber < 20) {
+            validationStatus = 'PASSED_VALIDATION';
+          } else if (randNumber > 80) {
+            validationStatus = 'FAILED_VALIDATION';
+          }
+          return {
+            normalizedTargetUrl: url,
+            status: validationStatus,
+            __typename: 'DastSiteValidation',
+          };
+        }),
+        __typename: 'DastSiteValidations',
+      };
     },
-  ),
+  },
+};
+
+export default new VueApollo({
+  defaultClient: createDefaultClient(resolvers, {
+    assumeImmutableResults: true,
+  }),
 });
diff --git a/ee/app/assets/javascripts/security_configuration/dast_site_validation/graphql/dast_site_validations.query.graphql b/ee/app/assets/javascripts/security_configuration/dast_site_validation/graphql/dast_site_validations.query.graphql
index faf7db8e7e4..f72720ad9a6 100644
--- a/ee/app/assets/javascripts/security_configuration/dast_site_validation/graphql/dast_site_validations.query.graphql
+++ b/ee/app/assets/javascripts/security_configuration/dast_site_validation/graphql/dast_site_validations.query.graphql
@@ -1,10 +1,8 @@
 query project($fullPath: ID!, $urls: [String!]) {
-  project(fullPath: $fullPath) {
-    validations: dastSiteValidations(normalizedTargetUrls: $urls) {
-      nodes {
-        normalizedTargetUrl
-        status
-      }
+  validations: dastSiteValidations(normalizedTargetUrls: $urls) @client {
+    nodes {
+      normalizedTargetUrl
+      status
     }
   }
 }
  1. Navigate to the the site profiles library at :group/:project/-/security/configuration/dast_profiles.
  2. Validate some profiles by clicking on the associated Validate target site button.
  3. Reload the page to try again.

Screenshots (strongly suggested)

validation_polling

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Related to #280570 (closed)

Edited by Paul Gascou-Vaillancourt

Merge request reports