From 2dbab45af129c3fcf83ecb70e64ccfe30c4183cf Mon Sep 17 00:00:00 2001
From: "Alan (Maciej) Paruszewski" <mparuszewski@gitlab.com>
Date: Tue, 6 Jul 2021 07:33:01 +0000
Subject: [PATCH] Add new security job artifact report cluster image scanning

---
 app/models/ci/job_artifact.rb                 |  6 +-
 doc/api/graphql/reference/index.md            |  1 +
 doc/ci/yaml/index.md                          | 12 +++
 .../types/vulnerability_location_type.rb      |  2 +-
 ee/app/models/ee/ci/build.rb                  |  1 +
 ee/app/models/ee/ci/job_artifact.rb           |  7 +-
 ee/app/models/ee/ci/pipeline.rb               |  1 +
 ee/app/models/ee/namespace.rb                 |  1 +
 ee/app/models/license.rb                      |  1 +
 ee/lib/ee/gitlab/ci/parsers.rb                |  1 +
 ee/lib/gitlab/vulnerabilities/parser.rb       |  2 +-
 ee/spec/factories/ci/builds.rb                | 14 ++-
 ee/spec/factories/ci/job_artifacts.rb         | 30 ++++++
 ee/spec/factories/ci/pipelines.rb             | 18 +++-
 ee/spec/factories/vulnerabilities/feedback.rb |  4 +
 .../gl-cluster-image-scanning-report.json     | 56 ++++++++++++
 .../gl-cluster-image-scanning-report.json     | 91 +++++++++++++++++++
 .../lib/gitlab/vulnerabilities/parser_spec.rb |  9 ++
 ee/spec/models/ci/pipeline_spec.rb            | 14 ++-
 ee/spec/models/ee/ci/job_artifact_spec.rb     | 23 +++--
 ee/spec/models/ee/namespace_spec.rb           |  2 +-
 .../graphql/vulnerabilities/location_spec.rb  | 39 ++++++++
 .../store_security_reports_worker_spec.rb     |  2 +-
 lib/gitlab/ci/config/entry/reports.rb         |  3 +-
 spec/factories/ci/builds.rb                   |  8 ++
 .../gitlab/ci/config/entry/reports_spec.rb    |  1 +
 spec/services/ci/retry_build_service_spec.rb  |  2 +-
 27 files changed, 332 insertions(+), 19 deletions(-)
 create mode 100644 ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json
 create mode 100644 ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json

diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ec6858892a21f..46c976d5616ec 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -33,6 +33,7 @@ class JobArtifact < ApplicationRecord
       secret_detection: 'gl-secret-detection-report.json',
       dependency_scanning: 'gl-dependency-scanning-report.json',
       container_scanning: 'gl-container-scanning-report.json',
+      cluster_image_scanning: 'gl-cluster-image-scanning-report.json',
       dast: 'gl-dast-report.json',
       license_scanning: 'gl-license-scanning-report.json',
       performance: 'performance.json',
@@ -71,6 +72,7 @@ class JobArtifact < ApplicationRecord
       secret_detection: :raw,
       dependency_scanning: :raw,
       container_scanning: :raw,
+      cluster_image_scanning: :raw,
       dast: :raw,
       license_scanning: :raw,
 
@@ -108,6 +110,7 @@ class JobArtifact < ApplicationRecord
       sast
       secret_detection
       requirements
+      cluster_image_scanning
     ].freeze
 
     TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
@@ -212,7 +215,8 @@ class JobArtifact < ApplicationRecord
       coverage_fuzzing: 23, ## EE-specific
       browser_performance: 24, ## EE-specific
       load_performance: 25, ## EE-specific
-      api_fuzzing: 26 ## EE-specific
+      api_fuzzing: 26, ## EE-specific
+      cluster_image_scanning: 27 ## EE-specific
     }
 
     # `file_location` indicates where actual files are stored.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4a9c75967a3f1..5f8883dde5746 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14547,6 +14547,7 @@ Iteration ID wildcard values.
 | <a id="jobartifactfiletypearchive"></a>`ARCHIVE` | ARCHIVE job artifact file type. |
 | <a id="jobartifactfiletypebrowser_performance"></a>`BROWSER_PERFORMANCE` | BROWSER PERFORMANCE job artifact file type. |
 | <a id="jobartifactfiletypecluster_applications"></a>`CLUSTER_APPLICATIONS` | CLUSTER APPLICATIONS job artifact file type. |
+| <a id="jobartifactfiletypecluster_image_scanning"></a>`CLUSTER_IMAGE_SCANNING` | CLUSTER IMAGE SCANNING job artifact file type. |
 | <a id="jobartifactfiletypecobertura"></a>`COBERTURA` | COBERTURA job artifact file type. |
 | <a id="jobartifactfiletypecodequality"></a>`CODEQUALITY` | CODE QUALITY job artifact file type. |
 | <a id="jobartifactfiletypecontainer_scanning"></a>`CONTAINER_SCANNING` | CONTAINER SCANNING job artifact file type. |
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index f5f7d427cd909..f927393e80961 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -3064,6 +3064,18 @@ as artifacts.
 The collected coverage fuzzing report uploads to GitLab as an artifact and is summarized in merge
 requests and the pipeline view. It's also used to provide data for security dashboards.
 
+##### `artifacts:reports:cluster_image_scanning` **(ULTIMATE)**
+
+> - Introduced in GitLab 14.1.
+> - Requires GitLab Runner 14.1 and above.
+
+The `cluster_image_scanning` report collects `CLUSTER_IMAGE_SCANNING` vulnerabilities
+as artifacts.
+
+The collected `CLUSTER_IMAGE_SCANNING` report uploads to GitLab as an artifact and
+is summarized in the pipeline view. It's also used to provide data for security
+dashboards.
+
 ##### `artifacts:reports:dast` **(ULTIMATE)**
 
 > - Introduced in GitLab 11.5.
diff --git a/ee/app/graphql/types/vulnerability_location_type.rb b/ee/app/graphql/types/vulnerability_location_type.rb
index 2580c6f6c7bb1..897f4a876b9ec 100644
--- a/ee/app/graphql/types/vulnerability_location_type.rb
+++ b/ee/app/graphql/types/vulnerability_location_type.rb
@@ -16,7 +16,7 @@ class VulnerabilityLocationType < BaseUnion
 
     def self.resolve_type(object, context)
       case object[:report_type]
-      when 'container_scanning'
+      when 'container_scanning', 'cluster_image_scanning'
         VulnerabilityLocation::ContainerScanningType
       when 'dependency_scanning'
         VulnerabilityLocation::DependencyScanningType
diff --git a/ee/app/models/ee/ci/build.rb b/ee/app/models/ee/ci/build.rb
index e117908b7a5a1..5dabacf4bc045 100644
--- a/ee/app/models/ee/ci/build.rb
+++ b/ee/app/models/ee/ci/build.rb
@@ -16,6 +16,7 @@ module Build
         secret_detection: :secret_detection,
         dependency_scanning: :dependency_scanning,
         container_scanning: :container_scanning,
+        cluster_image_scanning: :cluster_image_scanning,
         dast: :dast,
         coverage_fuzzing: :coverage_fuzzing,
         api_fuzzing: :api_fuzzing
diff --git a/ee/app/models/ee/ci/job_artifact.rb b/ee/app/models/ee/ci/job_artifact.rb
index dd80b203525f8..c46a694ef4880 100644
--- a/ee/app/models/ee/ci/job_artifact.rb
+++ b/ee/app/models/ee/ci/job_artifact.rb
@@ -15,11 +15,12 @@ module Ci::JobArtifact
       # See https://gitlab.com/gitlab-org/gitlab/-/issues/297472
       after_destroy :log_geo_deleted_event
 
-      SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning dast coverage_fuzzing api_fuzzing].freeze
+      SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning cluster_image_scanning dast coverage_fuzzing api_fuzzing].freeze
       LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_scanning].freeze
       DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze
       METRICS_REPORT_FILE_TYPES = %w[metrics].freeze
       CONTAINER_SCANNING_REPORT_TYPES = %w[container_scanning].freeze
+      CLUSTER_IMAGE_SCANNING_REPORT_TYPES = %w[cluster_image_scanning].freeze
       DAST_REPORT_TYPES = %w[dast].freeze
       REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze
       COVERAGE_FUZZING_REPORT_TYPES = %w[coverage_fuzzing].freeze
@@ -44,6 +45,10 @@ module Ci::JobArtifact
         with_file_types(CONTAINER_SCANNING_REPORT_TYPES)
       end
 
+      scope :cluster_image_scanning_reports, -> do
+        with_file_types(CLUSTER_IMAGE_SCANNING_REPORT_TYPES)
+      end
+
       scope :dast_reports, -> do
         with_file_types(DAST_REPORT_TYPES)
       end
diff --git a/ee/app/models/ee/ci/pipeline.rb b/ee/app/models/ee/ci/pipeline.rb
index 95a5db5253936..da84ed7112eef 100644
--- a/ee/app/models/ee/ci/pipeline.rb
+++ b/ee/app/models/ee/ci/pipeline.rb
@@ -46,6 +46,7 @@ module Pipeline
           secret_detection: %i[secret_detection],
           dependency_scanning: %i[dependency_scanning],
           container_scanning: %i[container_scanning],
+          cluster_image_scanning: %i[cluster_image_scanning],
           dast: %i[dast],
           performance: %i[merge_request_performance_metrics],
           browser_performance: %i[merge_request_performance_metrics],
diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb
index 8f2778419e317..52d71558fb804 100644
--- a/ee/app/models/ee/namespace.rb
+++ b/ee/app/models/ee/namespace.rb
@@ -332,6 +332,7 @@ def store_security_reports_available?
       feature_available?(:secret_detection) ||
       feature_available?(:dependency_scanning) ||
       feature_available?(:container_scanning) ||
+      feature_available?(:cluster_image_scanning) ||
       feature_available?(:dast) ||
       feature_available?(:coverage_fuzzing) ||
       feature_available?(:api_fuzzing)
diff --git a/ee/app/models/license.rb b/ee/app/models/license.rb
index 85ff0df9ca226..dff39512a8e5c 100644
--- a/ee/app/models/license.rb
+++ b/ee/app/models/license.rb
@@ -144,6 +144,7 @@ class License < ApplicationRecord
     api_fuzzing
     auto_rollback
     cilium_alerts
+    cluster_image_scanning
     external_status_checks
     container_scanning
     coverage_fuzzing
diff --git a/ee/lib/ee/gitlab/ci/parsers.rb b/ee/lib/ee/gitlab/ci/parsers.rb
index 058bedca7d827..39d845072b639 100644
--- a/ee/lib/ee/gitlab/ci/parsers.rb
+++ b/ee/lib/ee/gitlab/ci/parsers.rb
@@ -12,6 +12,7 @@ def parsers
                 license_scanning: ::Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning,
                 dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning,
                 container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
+                cluster_image_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
                 dast: ::Gitlab::Ci::Parsers::Security::Dast,
                 sast: ::Gitlab::Ci::Parsers::Security::Sast,
                 api_fuzzing: ::Gitlab::Ci::Parsers::Security::Dast,
diff --git a/ee/lib/gitlab/vulnerabilities/parser.rb b/ee/lib/gitlab/vulnerabilities/parser.rb
index 08daddd7e3926..bd05e914c09b7 100644
--- a/ee/lib/gitlab/vulnerabilities/parser.rb
+++ b/ee/lib/gitlab/vulnerabilities/parser.rb
@@ -25,7 +25,7 @@ def valid_categories
         end
 
         def standard_vulnerability?(category)
-          (valid_categories.keys - ['container_scanning']).include?(category)
+          (valid_categories.keys - %w[container_scanning cluster_image_scanning]).include?(category)
         end
       end
     end
diff --git a/ee/spec/factories/ci/builds.rb b/ee/spec/factories/ci/builds.rb
index dcc5687f7ad9e..da2ea8b21317d 100644
--- a/ee/spec/factories/ci/builds.rb
+++ b/ee/spec/factories/ci/builds.rb
@@ -6,7 +6,7 @@
       failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] }
     end
 
-    %i[api_fuzzing codequality container_scanning dast dependency_scanning license_scanning performance browser_performance load_performance sast secret_detection coverage_fuzzing].each do |report_type|
+    %i[api_fuzzing codequality container_scanning cluster_image_scanning dast dependency_scanning license_scanning performance browser_performance load_performance sast secret_detection coverage_fuzzing].each do |report_type|
       trait "legacy_#{report_type}".to_sym do
         success
         artifacts
@@ -84,6 +84,18 @@
       end
     end
 
+    trait :cluster_image_scanning_feature_branch do
+      after(:build) do |build|
+        build.job_artifacts << create(:ee_ci_job_artifact, :cluster_image_scanning_feature_branch, job: build)
+      end
+    end
+
+    trait :corrupted_cluster_image_scanning_report do
+      after(:build) do |build|
+        build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_cluster_image_scanning_report, job: build)
+      end
+    end
+
     trait :dependency_scanning_feature_branch do
       after(:build) do |build|
         build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
diff --git a/ee/spec/factories/ci/job_artifacts.rb b/ee/spec/factories/ci/job_artifacts.rb
index 189ea29adb519..e19e8062ca292 100644
--- a/ee/spec/factories/ci/job_artifacts.rb
+++ b/ee/spec/factories/ci/job_artifacts.rb
@@ -299,6 +299,16 @@
       end
     end
 
+    trait :cluster_image_scanning do
+      file_format { :raw }
+      file_type { :cluster_image_scanning }
+
+      after(:build) do |artifact, _|
+        artifact.file = fixture_file_upload(
+          Rails.root.join('ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json'), 'application/json')
+      end
+    end
+
     trait :common_security_report do
       file_format { :raw }
       file_type { :dependency_scanning }
@@ -339,6 +349,26 @@
       end
     end
 
+    trait :cluster_image_scanning_feature_branch do
+      file_format { :raw }
+      file_type { :cluster_image_scanning }
+
+      after(:build) do |artifact, _|
+        artifact.file = fixture_file_upload(
+          Rails.root.join('ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json'), 'application/json')
+      end
+    end
+
+    trait :corrupted_cluster_image_scanning_report do
+      file_format { :raw }
+      file_type { :cluster_image_scanning }
+
+      after(:build) do |artifact, _|
+        artifact.file = fixture_file_upload(
+          Rails.root.join('spec/fixtures/trace/sample_trace'), 'application/json')
+      end
+    end
+
     trait :metrics do
       file_format { :gzip }
       file_type { :metrics }
diff --git a/ee/spec/factories/ci/pipelines.rb b/ee/spec/factories/ci/pipelines.rb
index 9dad575d039fa..e3e5d555cce80 100644
--- a/ee/spec/factories/ci/pipelines.rb
+++ b/ee/spec/factories/ci/pipelines.rb
@@ -2,7 +2,7 @@
 
 FactoryBot.define do
   factory :ee_ci_pipeline, class: 'Ci::Pipeline', parent: :ci_pipeline do
-    %i[api_fuzzing browser_performance codequality container_scanning coverage_fuzzing dast dependency_list dependency_scanning license_scanning load_performance sast secret_detection].each do |report_type|
+    %i[api_fuzzing browser_performance codequality container_scanning cluster_image_scanning coverage_fuzzing dast dependency_list dependency_scanning license_scanning load_performance sast secret_detection].each do |report_type|
       trait "with_#{report_type}_report".to_sym do
         status { :success }
 
@@ -28,6 +28,22 @@
       end
     end
 
+    trait :with_cluster_image_scanning_feature_branch do
+      status { :success }
+
+      after(:build) do |pipeline, evaluator|
+        pipeline.builds << build(:ee_ci_build, :cluster_image_scanning_feature_branch, pipeline: pipeline, project: pipeline.project)
+      end
+    end
+
+    trait :with_corrupted_cluster_image_scanning_report do
+      status { :success }
+
+      after(:build) do |pipeline, evaluator|
+        pipeline.builds << build(:ee_ci_build, :corrupted_cluster_image_scanning_report, pipeline: pipeline, project: pipeline.project)
+      end
+    end
+
     trait :with_dependency_scanning_feature_branch do
       status { :success }
 
diff --git a/ee/spec/factories/vulnerabilities/feedback.rb b/ee/spec/factories/vulnerabilities/feedback.rb
index 34b81bd469d99..66495bc67bbec 100644
--- a/ee/spec/factories/vulnerabilities/feedback.rb
+++ b/ee/spec/factories/vulnerabilities/feedback.rb
@@ -52,6 +52,10 @@
       category { 'container_scanning' }
     end
 
+    trait :cluster_image_scanning do
+      category { 'cluster_image_scanning' }
+    end
+
     trait :dast do
       category { 'dast' }
     end
diff --git a/ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json b/ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json
new file mode 100644
index 0000000000000..7fd44870cc615
--- /dev/null
+++ b/ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json
@@ -0,0 +1,56 @@
+{
+  "version": "2.4",
+  "vulnerabilities": [
+    {
+      "id": "e987fa54ff94e1d0e716814861459d2eb10bd27a0ba8ca243428669d8885ce68",
+      "category": "cluster_image_scanning",
+      "message": "CVE-2017-15650 in musl",
+      "description": "musl:1.1.18-r3 is affected by CVE-2017-15650",
+      "cve": "alpine:v3.7:musl:CVE-2017-15650",
+      "severity": "High",
+      "confidence": "Unknown",
+      "solution": "Upgrade musl from 1.1.18-r3 to 1.1.18-r4",
+      "scanner": {
+        "id": "starboard",
+        "name": "Starboard"
+      },
+      "location": {
+        "dependency": {
+          "package": {
+            "name": "musl"
+          },
+          "version": "1.1.18-r3"
+        },
+        "operating_system": "alpine:v3.7",
+        "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583"
+      },
+      "identifiers": [
+        {
+          "type": "cve",
+          "name": "CVE-2017-15650",
+          "value": "CVE-2017-15650",
+          "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
+        }
+      ],
+      "links": [
+        {
+          "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
+        }
+      ]
+    }
+  ],
+  "remediations": [],
+  "scan": {
+    "scanner": {
+      "id": "starboard",
+      "name": "Starboard",
+      "url": "https://github.com/aquasecurity/starboard",
+      "vendor": {
+        "name": "GitLab"
+      },
+      "version": "2.1.4"
+    },
+    "type": "cluster_image_scanning",
+    "status": "success"
+  }
+}
diff --git a/ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json b/ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json
new file mode 100644
index 0000000000000..c8fa795d6479a
--- /dev/null
+++ b/ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json
@@ -0,0 +1,91 @@
+{
+  "version": "2.3",
+  "vulnerabilities": [
+    {
+      "category": "cluster_image_scanning",
+      "message": "CVE-2017-18269 in glibc",
+      "description": "An SSE2-optimized memmove implementation for i386 in sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S in the GNU C Library (aka glibc or libc6) 2.21 through 2.27 does not correctly perform the overlapping memory check if the source memory range spans the middle of the address space, resulting in corrupt data being produced by the copy operation. This may disclose information to context-dependent attackers, or result in a denial of service, or, possibly, code execution.",
+      "cve": "debian:9:glibc:CVE-2017-18269",
+      "severity": "Critical",
+      "confidence": "Unknown",
+      "solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
+      "scanner": {
+        "id": "starboard",
+        "name": "Starboard"
+      },
+      "location": {
+        "dependency": {
+          "package": {
+            "name": "glibc"
+          },
+          "version": "2.24-11+deb9u3"
+        },
+        "operating_system": "debian:9",
+        "image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
+      },
+      "identifiers": [
+        {
+          "type": "cve",
+          "name": "CVE-2017-18269",
+          "value": "CVE-2017-18269",
+          "url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
+        }
+      ],
+      "links": [
+        {
+          "url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
+        }
+      ]
+    },
+    {
+      "category": "cluster_image_scanning",
+      "message": "CVE-2017-16997 in glibc",
+      "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
+      "cve": "debian:9:glibc:CVE-2017-16997",
+      "severity": "Critical",
+      "confidence": "Unknown",
+      "solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
+      "scanner": {
+        "id": "starboard",
+        "name": "Starboard"
+      },
+      "location": {
+        "dependency": {
+          "package": {
+            "name": "glibc"
+          },
+          "version": "2.24-11+deb9u3"
+        },
+        "operating_system": "debian:9",
+        "image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
+      },
+      "identifiers": [
+        {
+          "type": "cve",
+          "name": "CVE-2017-16997",
+          "value": "CVE-2017-16997",
+          "url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
+        }
+      ],
+      "links": [
+        {
+          "url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
+        }
+      ]
+    }
+  ],
+  "remediations": [],
+  "scan": {
+    "scanner": {
+      "id": "starboard",
+      "name": "Starboard",
+      "url": "https://github.com/aquasecurity/starboard",
+      "vendor": {
+        "name": "GitLab"
+      },
+      "version": "0.10.0"
+    },
+    "type": "cluster_image_scanning",
+    "status": "success"
+  }
+}
diff --git a/ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb b/ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb
index 5f7499f047518..2328b5cb2c403 100644
--- a/ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb
+++ b/ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb
@@ -39,6 +39,15 @@
       end
     end
 
+    context 'with cluster image scanning as category' do
+      it 'returns a Scanning Vulnerability' do
+        params[:category] = 'cluster_image_scanning'
+
+        expect(subject).to be_a(Gitlab::Vulnerabilities::ContainerScanningVulnerability)
+        expect(subject.target_branch).to eq('master')
+      end
+    end
+
     context 'with an invalid category' do
       it 'raises an exception' do
         params[:category] = 'foo'
diff --git a/ee/spec/models/ci/pipeline_spec.rb b/ee/spec/models/ci/pipeline_spec.rb
index 4ca156dc697b5..21469a2b06671 100644
--- a/ee/spec/models/ci/pipeline_spec.rb
+++ b/ee/spec/models/ci/pipeline_spec.rb
@@ -109,7 +109,7 @@
     subject { pipeline.security_reports }
 
     before do
-      stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true)
+      stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, cluster_image_scanning: true)
     end
 
     context 'when pipeline has multiple builds with security reports' do
@@ -119,12 +119,16 @@
       let(:build_ds_2) { create(:ci_build, :success, name: 'ds_2', pipeline: pipeline, project: project) }
       let(:build_cs_1) { create(:ci_build, :success, name: 'cs_1', pipeline: pipeline, project: project) }
       let(:build_cs_2) { create(:ci_build, :success, name: 'cs_2', pipeline: pipeline, project: project) }
+      let(:build_cis_1) { create(:ci_build, :success, name: 'cis_1', pipeline: pipeline, project: project) }
+      let(:build_cis_2) { create(:ci_build, :success, name: 'cis_2', pipeline: pipeline, project: project) }
       let!(:sast1_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_1, project: project) }
       let!(:sast2_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_2, project: project) }
       let!(:ds1_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_1, project: project) }
       let!(:ds2_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_2, project: project) }
       let!(:cs1_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_1, project: project) }
       let!(:cs2_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_2, project: project) }
+      let!(:cis1_artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning, job: build_cis_1, project: project) }
+      let!(:cis2_artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning, job: build_cis_2, project: project) }
 
       it 'assigns pipeline to the reports' do
         expect(subject.pipeline).to eq(pipeline)
@@ -132,12 +136,13 @@
       end
 
       it 'returns security reports with collected data grouped as expected' do
-        expect(subject.reports.keys).to contain_exactly('sast', 'dependency_scanning', 'container_scanning')
+        expect(subject.reports.keys).to contain_exactly('sast', 'dependency_scanning', 'container_scanning', 'cluster_image_scanning')
 
         # for each of report categories, we have merged 2 reports with the same data (fixture)
         expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5)
         expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4)
         expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8)
+        expect(subject.get_report('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
       end
 
       context 'when builds are retried' do
@@ -147,6 +152,7 @@
           expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5)
           expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4)
           expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8)
+          expect(subject.get_report('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
         end
       end
 
@@ -535,18 +541,22 @@
     where(:pipeline_status, :build_types, :expected_status) do
       [
         [:blocked, [:container_scanning], false],
+        [:blocked, [:cluster_image_scanning], false],
         [:blocked, [:license_scan_v2_1, :container_scanning], true],
         [:blocked, [:license_scan_v2_1], true],
         [:blocked, [], false],
         [:failed, [:container_scanning], false],
+        [:failed, [:cluster_image_scanning], false],
         [:failed, [:license_scan_v2_1, :container_scanning], true],
         [:failed, [:license_scan_v2_1], true],
         [:failed, [], false],
         [:running, [:container_scanning], false],
+        [:running, [:cluster_image_scanning], false],
         [:running, [:license_scan_v2_1, :container_scanning], true],
         [:running, [:license_scan_v2_1], true],
         [:running, [], false],
         [:success, [:container_scanning], false],
+        [:success, [:cluster_image_scanning], false],
         [:success, [:license_scan_v2_1, :container_scanning], true],
         [:success, [:license_scan_v2_1], true],
         [:success, [], false]
diff --git a/ee/spec/models/ee/ci/job_artifact_spec.rb b/ee/spec/models/ee/ci/job_artifact_spec.rb
index 4514b15fc742f..1b59e6e1a56ce 100644
--- a/ee/spec/models/ee/ci/job_artifact_spec.rb
+++ b/ee/spec/models/ee/ci/job_artifact_spec.rb
@@ -31,6 +31,14 @@
     it { is_expected.to eq([artifact]) }
   end
 
+  describe '.cluster_image_scanning_reports' do
+    subject { Ci::JobArtifact.cluster_image_scanning_reports }
+
+    let_it_be(:artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning) }
+
+    it { is_expected.to eq([artifact]) }
+  end
+
   describe '.metrics_reports' do
     subject { Ci::JobArtifact.metrics_reports }
 
@@ -226,13 +234,14 @@
 
     context 'for different types' do
       where(:file_type, :security_report?) do
-        :performance         | false
-        :sast                | true
-        :secret_detection    | true
-        :dependency_scanning | true
-        :container_scanning  | true
-        :dast                | true
-        :coverage_fuzzing    | true
+        :performance            | false
+        :sast                   | true
+        :secret_detection       | true
+        :dependency_scanning    | true
+        :container_scanning     | true
+        :cluster_image_scanning | true
+        :dast                   | true
+        :coverage_fuzzing       | true
       end
 
       with_them do
diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb
index bc5e6e770763c..8ad06f32e2a4d 100644
--- a/ee/spec/models/ee/namespace_spec.rb
+++ b/ee/spec/models/ee/namespace_spec.rb
@@ -1314,7 +1314,7 @@
     subject { namespace.store_security_reports_available? }
 
     context 'when at least one security report feature is enabled' do
-      where(report_type: [:sast, :secret_detection, :dast, :dependency_scanning, :container_scanning])
+      where(report_type: [:sast, :secret_detection, :dast, :dependency_scanning, :container_scanning, :cluster_image_scanning])
 
       with_them do
         before do
diff --git a/ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb b/ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb
index 0c906af370eaf..2bdf8fa17ad6e 100644
--- a/ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb
+++ b/ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb
@@ -110,6 +110,45 @@
     end
   end
 
+  context 'when the vulnerability was found by a cluster image scan' do
+    let_it_be(:vulnerability) do
+      create(:vulnerability, project: project, report_type: :cluster_image_scanning)
+    end
+
+    let_it_be(:metadata) do
+      {
+        location: {
+          image: 'vulnerable_image',
+          operating_system: 'vulnerable_os',
+          dependency: {
+            version: '6.6.6',
+            package: {
+              name: 'vulnerable_container'
+            }
+          }
+        }
+      }
+    end
+
+    let_it_be(:finding) do
+      create(
+        :vulnerabilities_finding,
+        vulnerability: vulnerability,
+        raw_metadata: metadata.to_json
+      )
+    end
+
+    it 'returns a container location' do
+      location = subject.first['location']
+
+      expect(location['__typename']).to eq('VulnerabilityLocationContainerScanning')
+      expect(location['image']).to eq('vulnerable_image')
+      expect(location['operatingSystem']).to eq('vulnerable_os')
+      expect(location['dependency']['version']).to eq('6.6.6')
+      expect(location['dependency']['package']['name']).to eq('vulnerable_container')
+    end
+  end
+
   context 'when the vulnerability was found by a dependency scan' do
     let_it_be(:vulnerability) do
       create(:vulnerability, project: project, report_type: :dependency_scanning)
diff --git a/ee/spec/workers/store_security_reports_worker_spec.rb b/ee/spec/workers/store_security_reports_worker_spec.rb
index 1d54ce5e69b96..ba2f51c43d1ff 100644
--- a/ee/spec/workers/store_security_reports_worker_spec.rb
+++ b/ee/spec/workers/store_security_reports_worker_spec.rb
@@ -56,7 +56,7 @@
     end
 
     context 'when at least one security report feature is enabled' do
-      where(report_type: [:sast, :dast, :dependency_scanning, :container_scanning])
+      where(report_type: [:sast, :dast, :dependency_scanning, :container_scanning, :cluster_image_scanning])
 
       with_them do
         before do
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 4db25fb09308a..e45dbfa243fda 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -15,7 +15,7 @@ class Reports < ::Gitlab::Config::Entry::Node
             %i[junit codequality sast secret_detection dependency_scanning container_scanning
                dast performance browser_performance load_performance license_scanning metrics lsif
                dotenv cobertura terraform accessibility cluster_applications
-               requirements coverage_fuzzing api_fuzzing].freeze
+               requirements coverage_fuzzing api_fuzzing cluster_image_scanning].freeze
 
           attributes ALLOWED_KEYS
 
@@ -32,6 +32,7 @@ class Reports < ::Gitlab::Config::Entry::Node
               validates :secret_detection, array_of_strings_or_string: true
               validates :dependency_scanning, array_of_strings_or_string: true
               validates :container_scanning, array_of_strings_or_string: true
+              validates :cluster_image_scanning, array_of_strings_or_string: true
               validates :dast, array_of_strings_or_string: true
               validates :performance, array_of_strings_or_string: true
               validates :browser_performance, array_of_strings_or_string: true
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 395d3ea598c00..0e535aeaa8d79 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -508,6 +508,14 @@
       end
     end
 
+    trait :cluster_image_scanning do
+      options do
+        {
+            artifacts: { reports: { cluster_image_scanning: 'gl-cluster-image-scanning-report.json' } }
+        }
+      end
+    end
+
     trait :license_scanning do
       options do
         {
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index d8907f7015b0a..12b8960eb329d 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -40,6 +40,7 @@
         :secret_detection | 'gl-secret-detection-report.json'
         :dependency_scanning | 'gl-dependency-scanning-report.json'
         :container_scanning | 'gl-container-scanning-report.json'
+        :cluster_image_scanning | 'gl-cluster-image-scanning-report.json'
         :dast | 'gl-dast-report.json'
         :license_scanning | 'gl-license-scanning-report.json'
         :performance | 'performance.json'
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 87893f11ed542..42d6e66b38b96 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -39,7 +39,7 @@
        erased_at auto_canceled_by job_artifacts job_artifacts_archive
        job_artifacts_metadata job_artifacts_trace job_artifacts_junit
        job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning
-       job_artifacts_container_scanning job_artifacts_dast
+       job_artifacts_container_scanning job_artifacts_cluster_image_scanning job_artifacts_dast
        job_artifacts_license_scanning
        job_artifacts_performance job_artifacts_browser_performance job_artifacts_load_performance
        job_artifacts_lsif job_artifacts_terraform job_artifacts_cluster_applications
-- 
GitLab