feat: add JFrog Xray vulnerability scanning module (#410)
This PR adds a new Terraform module that fetches JFrog Xray
vulnerability scanning results for container images stored in
Artifactory.
## Features
- Fetches vulnerability scan results from JFrog Xray
- Outputs vulnerability counts (Critical, High, Medium, Low, Total)
- Supports flexible image path formats
- Works with any workspace type using container images
- Provides secure token handling
## Design Decisions
During testing, we found two issues with the original approach of
defining the `xray` provider and `coder_metadata` inside the module:
1. **`coder_metadata` defined inside modules does not display in the
Coder dashboard** — this is a known limitation
2. **Inline provider blocks prevent using `count`/`for_each` on the
module** — which is needed when attaching metadata to resources like
`docker_container` that use `start_count`
The module now **outputs** vulnerability counts instead, and the caller
creates the `coder_metadata` and configures the `xray` provider in their
root template. This matches the pattern used by other registry modules.
## Usage
```hcl
provider "xray" {
url = "${var.jfrog_url}/xray"
access_token = var.artifactory_access_token
skip_xray_version_check = true
}
module "jfrog_xray" {
source = "registry.coder.com/coder/jfrog-xray/coder"
version = "1.0.0"
xray_url = "${var.jfrog_url}/xray"
xray_token = var.artifactory_access_token
image = "docker-local/codercom/enterprise-base:latest"
}
resource "coder_metadata" "xray_vulnerabilities" {
count = data.coder_workspace.me.start_count
resource_id = docker_container.workspace[0].id
icon = "/icon/shield.svg"
item {
key = "Total Vulnerabilities"
value = module.jfrog_xray.total
}
item {
key = "Critical"
value = module.jfrog_xray.critical
}
item {
key = "High"
value = module.jfrog_xray.high
}
item {
key = "Medium"
value = module.jfrog_xray.medium
}
item {
key = "Low"
value = module.jfrog_xray.low
}
}
```
## Related Issues
- Resolves coder/coder#12838
- Addresses coder/registry#65
Tested with a JFrog Cloud trial instance using Docker remote repository
and Xray scanning.
---------
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: matifali <10648092+matifali@users.noreply.github.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
This commit is contained in:
parent
f1748c80f7
commit
40c2916fa9
10
.icons/jfrog-xray.svg
Normal file
10
.icons/jfrog-xray.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<g fill="#40BE46">
|
||||||
|
<!-- Eye shape -->
|
||||||
|
<path d="M100 40C55 40 20 80 10 100c10 20 45 60 90 60s80-40 90-60c-10-20-45-60-90-60zm0 100c-35 0-63-28-75-40 12-12 40-40 75-40s63 28 75 40c-12 12-40 40-75 40z"/>
|
||||||
|
<!-- Inner circle (magnifying glass lens) -->
|
||||||
|
<path d="M100 72a28 28 0 1 0 0 56 28 28 0 0 0 0-56zm0 44a16 16 0 1 1 0-32 16 16 0 0 1 0 32z"/>
|
||||||
|
<!-- Horizontal line below -->
|
||||||
|
<rect x="25" y="170" width="150" height="12" rx="6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 542 B |
75
registry/coder/modules/jfrog-xray/README.md
Normal file
75
registry/coder/modules/jfrog-xray/README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
display_name: JFrog Xray
|
||||||
|
description: Fetch container image vulnerability scan results from JFrog Xray
|
||||||
|
icon: ../../../../.icons/jfrog-xray.svg
|
||||||
|
verified: true
|
||||||
|
tags: [jfrog, xray]
|
||||||
|
---
|
||||||
|
|
||||||
|
# JFrog Xray
|
||||||
|
|
||||||
|
This module fetches vulnerability scan results from JFrog Xray for container images stored in Artifactory. Use the outputs to display security information as workspace metadata.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jfrog_xray" {
|
||||||
|
source = "registry.coder.com/coder/jfrog-xray/coder"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
xray_url = "https://example.jfrog.io/xray"
|
||||||
|
xray_token = var.artifactory_access_token
|
||||||
|
image = "docker-local/myapp/backend:v1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_metadata" "xray_scan" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
resource_id = docker_container.workspace[0].id
|
||||||
|
icon = "/icon/shield.svg"
|
||||||
|
|
||||||
|
item {
|
||||||
|
key = "Image"
|
||||||
|
value = "docker-local/myapp/backend:v1.0.0"
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "Total Vulnerabilities"
|
||||||
|
value = module.jfrog_xray.total
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "Critical"
|
||||||
|
value = module.jfrog_xray.critical
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "High"
|
||||||
|
value = module.jfrog_xray.high
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "Medium"
|
||||||
|
value = module.jfrog_xray.medium
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "Low"
|
||||||
|
value = module.jfrog_xray.low
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Container images must be stored in JFrog Artifactory
|
||||||
|
2. JFrog Xray must be configured to scan your repositories
|
||||||
|
3. A valid JFrog access token with Xray read permissions
|
||||||
|
|
||||||
|
## Remote Repositories
|
||||||
|
|
||||||
|
When scanning images from remote (proxy) repositories, set `use_cache_repo = true`. This is because Artifactory stores cached images in a companion `-cache` repository where Xray indexes the scan results.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jfrog_xray" {
|
||||||
|
source = "registry.coder.com/coder/jfrog-xray/coder"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
xray_url = "https://example.jfrog.io/xray"
|
||||||
|
xray_token = var.artifactory_access_token
|
||||||
|
image = "docker-remote/library/nginx:latest"
|
||||||
|
use_cache_repo = true
|
||||||
|
}
|
||||||
|
```
|
||||||
244
registry/coder/modules/jfrog-xray/main.test.ts
Normal file
244
registry/coder/modules/jfrog-xray/main.test.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import { serve } from "bun";
|
||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { createJSONResponse, runTerraformInit, runTerraformApply } from "~test";
|
||||||
|
|
||||||
|
describe("jfrog-xray", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
// Mock server simulating a local repo with direct scan results
|
||||||
|
const mockLocalRepo = serve({
|
||||||
|
fetch: (req) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (url.pathname === "/xray/api/v1/system/version")
|
||||||
|
return createJSONResponse({
|
||||||
|
xray_version: "3.80.0",
|
||||||
|
xray_revision: "abc123",
|
||||||
|
});
|
||||||
|
if (url.pathname === "/xray/api/v1/artifacts")
|
||||||
|
return createJSONResponse({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "myapp/backend/v1.0.0",
|
||||||
|
repo_path: "/myapp/backend/v1.0.0/manifest.json",
|
||||||
|
size: "50.00 MB",
|
||||||
|
sec_issues: {
|
||||||
|
critical: 1,
|
||||||
|
high: 3,
|
||||||
|
medium: 5,
|
||||||
|
low: 10,
|
||||||
|
total: 19,
|
||||||
|
},
|
||||||
|
scans_status: {
|
||||||
|
overall: {
|
||||||
|
status: "DONE",
|
||||||
|
time: "2026-03-04T22:00:02Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
violations: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
return createJSONResponse({});
|
||||||
|
},
|
||||||
|
port: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock server simulating a remote repo with cache behavior
|
||||||
|
// Returns both tag manifest (0 vulns, 0 size) and SHA manifest (real vulns, real size)
|
||||||
|
const mockRemoteRepo = serve({
|
||||||
|
fetch: (req) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (url.pathname === "/xray/api/v1/system/version")
|
||||||
|
return createJSONResponse({
|
||||||
|
xray_version: "3.80.0",
|
||||||
|
xray_revision: "abc123",
|
||||||
|
});
|
||||||
|
if (url.pathname === "/xray/api/v1/artifacts")
|
||||||
|
return createJSONResponse({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "codercom/enterprise-base/ubuntu",
|
||||||
|
repo_path: "/codercom/enterprise-base/ubuntu/list.manifest.json",
|
||||||
|
size: "0.00 B",
|
||||||
|
sec_issues: { total: 0 },
|
||||||
|
scans_status: {
|
||||||
|
overall: { status: "DONE" },
|
||||||
|
},
|
||||||
|
violations: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "codercom/enterprise-base/sha256__abc123def456",
|
||||||
|
repo_path:
|
||||||
|
"/codercom/enterprise-base/sha256__abc123def456/manifest.json",
|
||||||
|
size: "359.33 MB",
|
||||||
|
sec_issues: {
|
||||||
|
critical: 2,
|
||||||
|
high: 6,
|
||||||
|
medium: 20,
|
||||||
|
low: 23,
|
||||||
|
total: 51,
|
||||||
|
},
|
||||||
|
scans_status: {
|
||||||
|
overall: { status: "DONE" },
|
||||||
|
},
|
||||||
|
violations: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
return createJSONResponse({});
|
||||||
|
},
|
||||||
|
port: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock server returning empty results (image not scanned)
|
||||||
|
const mockEmptyResults = serve({
|
||||||
|
fetch: (req) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (url.pathname === "/xray/api/v1/system/version")
|
||||||
|
return createJSONResponse({
|
||||||
|
xray_version: "3.80.0",
|
||||||
|
xray_revision: "abc123",
|
||||||
|
});
|
||||||
|
if (url.pathname === "/xray/api/v1/artifacts")
|
||||||
|
return createJSONResponse({ data: [], offset: -1 });
|
||||||
|
return createJSONResponse({});
|
||||||
|
},
|
||||||
|
port: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const localRepoUrl = `http://${mockLocalRepo.hostname}:${mockLocalRepo.port}`;
|
||||||
|
const remoteRepoUrl = `http://${mockRemoteRepo.hostname}:${mockRemoteRepo.port}`;
|
||||||
|
const emptyResultsUrl = `http://${mockEmptyResults.hostname}:${mockEmptyResults.port}`;
|
||||||
|
|
||||||
|
const getProviderEnv = (url: string) => ({
|
||||||
|
XRAY_URL: url,
|
||||||
|
XRAY_ACCESS_TOKEN: "test-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates required variable: xray_url", async () => {
|
||||||
|
try {
|
||||||
|
await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_token: "test-token",
|
||||||
|
image: "docker-local/test/image:latest",
|
||||||
|
},
|
||||||
|
getProviderEnv(localRepoUrl),
|
||||||
|
);
|
||||||
|
throw new Error("Expected apply to fail without xray_url");
|
||||||
|
} catch (ex) {
|
||||||
|
if (!(ex instanceof Error)) throw new Error("Unknown error");
|
||||||
|
expect(ex.message).toContain('input variable "xray_url" is not set');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates required variable: xray_token", async () => {
|
||||||
|
try {
|
||||||
|
await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: localRepoUrl,
|
||||||
|
image: "docker-local/test/image:latest",
|
||||||
|
},
|
||||||
|
getProviderEnv(localRepoUrl),
|
||||||
|
);
|
||||||
|
throw new Error("Expected apply to fail without xray_token");
|
||||||
|
} catch (ex) {
|
||||||
|
if (!(ex instanceof Error)) throw new Error("Unknown error");
|
||||||
|
expect(ex.message).toContain('input variable "xray_token" is not set');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates required variable: image", async () => {
|
||||||
|
try {
|
||||||
|
await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: localRepoUrl,
|
||||||
|
xray_token: "test-token",
|
||||||
|
},
|
||||||
|
getProviderEnv(localRepoUrl),
|
||||||
|
);
|
||||||
|
throw new Error("Expected apply to fail without image");
|
||||||
|
} catch (ex) {
|
||||||
|
if (!(ex instanceof Error)) throw new Error("Unknown error");
|
||||||
|
expect(ex.message).toContain('input variable "image" is not set');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns vulnerability counts for local repository", async () => {
|
||||||
|
const state = await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: localRepoUrl,
|
||||||
|
xray_token: "test-token",
|
||||||
|
image: "docker-local/myapp/backend:v1.0.0",
|
||||||
|
},
|
||||||
|
getProviderEnv(localRepoUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.outputs.critical.value).toBe(1);
|
||||||
|
expect(state.outputs.high.value).toBe(3);
|
||||||
|
expect(state.outputs.medium.value).toBe(5);
|
||||||
|
expect(state.outputs.low.value).toBe(10);
|
||||||
|
expect(state.outputs.total.value).toBe(19);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns zero counts when image has no scan results", async () => {
|
||||||
|
const state = await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: emptyResultsUrl,
|
||||||
|
xray_token: "test-token",
|
||||||
|
image: "docker-local/unscanned/image:latest",
|
||||||
|
},
|
||||||
|
getProviderEnv(emptyResultsUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.outputs.critical.value).toBe(0);
|
||||||
|
expect(state.outputs.high.value).toBe(0);
|
||||||
|
expect(state.outputs.medium.value).toBe(0);
|
||||||
|
expect(state.outputs.low.value).toBe(0);
|
||||||
|
expect(state.outputs.total.value).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses cache repo when use_cache_repo is enabled", async () => {
|
||||||
|
const state = await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: remoteRepoUrl,
|
||||||
|
xray_token: "test-token",
|
||||||
|
image: "docker-remote/codercom/enterprise-base:ubuntu",
|
||||||
|
use_cache_repo: true,
|
||||||
|
},
|
||||||
|
getProviderEnv(remoteRepoUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should find the SHA artifact with actual vulnerabilities
|
||||||
|
expect(state.outputs.critical.value).toBe(2);
|
||||||
|
expect(state.outputs.high.value).toBe(6);
|
||||||
|
expect(state.outputs.medium.value).toBe(20);
|
||||||
|
expect(state.outputs.low.value).toBe(23);
|
||||||
|
expect(state.outputs.total.value).toBe(51);
|
||||||
|
expect(state.outputs.violations.value).toBe(2);
|
||||||
|
expect(state.outputs.artifact_name.value).toContain("sha256__");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows custom repo and repo_path override", async () => {
|
||||||
|
const state = await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
xray_url: localRepoUrl,
|
||||||
|
xray_token: "test-token",
|
||||||
|
image: "ignored/path:tag",
|
||||||
|
repo: "docker-local",
|
||||||
|
repo_path: "/myapp/backend/v1.0.0",
|
||||||
|
},
|
||||||
|
getProviderEnv(localRepoUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.outputs.total.value).toBe(19);
|
||||||
|
});
|
||||||
|
});
|
||||||
135
registry/coder/modules/jfrog-xray/main.tf
Normal file
135
registry/coder/modules/jfrog-xray/main.tf
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
xray = {
|
||||||
|
source = "jfrog/xray"
|
||||||
|
version = ">= 2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "xray" {
|
||||||
|
url = var.xray_url
|
||||||
|
access_token = var.xray_token
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "xray_url" {
|
||||||
|
description = "The URL of your JFrog Xray instance (e.g., https://mycompany.jfrog.io/xray). This should point to the Xray API endpoint, not Artifactory."
|
||||||
|
type = string
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^https?://", var.xray_url))
|
||||||
|
error_message = "The xray_url must be a valid URL starting with http:// or https://."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "xray_token" {
|
||||||
|
description = "The access token for authenticating with JFrog Xray. This token needs read permissions on Xray scan results. You can generate one in JFrog Platform under User Management > Access Tokens."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image" {
|
||||||
|
description = "The Docker image to check for vulnerabilities, in the format 'repo/path/image:tag'. For example: 'docker-local/myapp/backend:v1.0.0' or 'docker-remote/library/nginx:latest'. The repository name is extracted from the first path segment."
|
||||||
|
type = string
|
||||||
|
validation {
|
||||||
|
condition = length(split("/", var.image)) >= 2
|
||||||
|
error_message = "The image must include at least a repository and image name (e.g., 'docker-local/myimage:tag')."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "repo" {
|
||||||
|
description = "Override the repository name extracted from the image path. Use this when your Artifactory repository name differs from the first segment of your image path."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "repo_path" {
|
||||||
|
description = "Override the full Xray repository path. Use this for custom path structures that don't follow the standard 'repo/image:tag' format. When set, this takes precedence over automatic path construction."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "use_cache_repo" {
|
||||||
|
description = "Set to true when scanning images from remote (proxy) repositories. Remote repositories in Artifactory store cached artifacts in a companion '-cache' repository (e.g., 'docker-remote-cache'), which is where Xray indexes the scan results."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Parse the image string into components
|
||||||
|
# Example: "docker-local/myapp/backend:v1.0.0"
|
||||||
|
# -> repo: "docker-local", image_name: "myapp/backend", tag: "v1.0.0"
|
||||||
|
image_parts = split("/", var.image)
|
||||||
|
base_repo = var.repo != "" ? var.repo : local.image_parts[0]
|
||||||
|
parsed_repo = var.use_cache_repo ? "${local.base_repo}-cache" : local.base_repo
|
||||||
|
image_path = join("/", slice(local.image_parts, 1, length(local.image_parts)))
|
||||||
|
image_name = split(":", local.image_path)[0]
|
||||||
|
image_tag = length(split(":", local.image_path)) > 1 ? split(":", local.image_path)[1] : "latest"
|
||||||
|
|
||||||
|
# Construct the Xray query path based on repository type:
|
||||||
|
# - Local repositories: Query the exact tag path (e.g., /myapp/backend/v1.0.0)
|
||||||
|
# - Remote repositories: Query by image name only (e.g., /myapp/backend) because
|
||||||
|
# the Terraform provider only returns the SHA manifest (with actual scan data)
|
||||||
|
# when querying the broader path
|
||||||
|
parsed_path = var.repo_path != "" ? var.repo_path : (
|
||||||
|
var.use_cache_repo ? "/${local.image_name}" : "/${local.image_name}/${local.image_tag}"
|
||||||
|
)
|
||||||
|
|
||||||
|
results = coalesce(try(data.xray_artifacts_scan.image_scan.results, []), [])
|
||||||
|
|
||||||
|
# For remote repositories, filter to find the actual scanned image (not tag pointers):
|
||||||
|
# - Tag manifests have size "0.00 B" (they're just pointers to SHA manifests)
|
||||||
|
# - SHA manifests have actual size (e.g., "359.33 MB") and contain the real scan data
|
||||||
|
# For local repositories, there's typically only one result which is the actual image
|
||||||
|
scanned_images = var.use_cache_repo ? [
|
||||||
|
for r in local.results : r if r.size != "0.00 B"
|
||||||
|
] : local.results
|
||||||
|
|
||||||
|
# The artifact we'll report scan results for
|
||||||
|
scan_result = (
|
||||||
|
length(local.scanned_images) > 0 ? local.scanned_images[0] :
|
||||||
|
length(local.results) > 0 ? local.results[0] :
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data "xray_artifacts_scan" "image_scan" {
|
||||||
|
repo = local.parsed_repo
|
||||||
|
repo_path = local.parsed_path
|
||||||
|
}
|
||||||
|
|
||||||
|
output "critical" {
|
||||||
|
description = "The number of critical severity vulnerabilities found in the image. Critical vulnerabilities typically require immediate attention."
|
||||||
|
value = try(local.scan_result.sec_issues.critical, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "high" {
|
||||||
|
description = "The number of high severity vulnerabilities found in the image."
|
||||||
|
value = try(local.scan_result.sec_issues.high, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "medium" {
|
||||||
|
description = "The number of medium severity vulnerabilities found in the image."
|
||||||
|
value = try(local.scan_result.sec_issues.medium, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "low" {
|
||||||
|
description = "The number of low severity vulnerabilities found in the image."
|
||||||
|
value = try(local.scan_result.sec_issues.low, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "total" {
|
||||||
|
description = "The total number of vulnerabilities found across all severity levels."
|
||||||
|
value = try(local.scan_result.sec_issues.total, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "artifact_name" {
|
||||||
|
description = "The name of the artifact that was scanned, as reported by Xray. For remote repositories, this will be the SHA-based manifest name (e.g., 'myimage/sha256__abc123...')."
|
||||||
|
value = try(local.scan_result.name, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
output "violations" {
|
||||||
|
description = "The number of Xray policy violations detected. Violations are triggered when vulnerabilities match rules defined in your Xray security policies."
|
||||||
|
value = try(local.scan_result.violations, 0)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user