SsvcPolicy
How the tenant makes its CISA SSVC decision (https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc) for a finding: Track, Track*. Fields, YAML, HCL and CLI samples, and how apply treats it.
How the tenant makes its CISA SSVC decision (https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc) for a finding: Track, Track*, Attend or Act. The decision table is the CISA Coordinator table 2.0.3 and is fixed: four decision points (exploitation, automatable, technical impact, mission and well-being) go in. This document configures how Vulnetix’s signals become those four values (CEL over the finding variables, and the repository’s criticality for mission and well-being), which table cells the organisation reads differently (mapping), which findings it decides by rule instead (overrides), and what each outcome asks of it (outcomes). A field left out takes the default, which is the policy of a tenant with no document. One per tenant.
apiVersion |
vulnetix.com/v1 |
kind |
SsvcPolicy |
| Group | Prioritisation |
| Status | Served. The config engine plans and applies it. |
| Objects per tenant | one (metadata.name: default) |
| Token and role | An owner or admin session, or a vmt_ config management token with the owner or admin role that lists SsvcPolicy in its kinds. See Management tokens. |
| CEL fields | spec.exploitation.active (finding), spec.exploitation.poc (finding), spec.automatable (finding), spec.technicalImpact (finding), spec.overrides[].when (finding): see CEL rule expressions |
| Terraform resource | vulnetix_ssvc_policy, imported by name (see Terraform and OpenTofu) |
| CLI | vulnetix config apply, CLI v3.168.0 and later. |
| Pinned schema | v1.15.0/SsvcPolicy.schema.json |
| Newest schema | latest/SsvcPolicy.schema.json |
| First released in | v1.8.0 |
| Print the built-in copy | vulnetix config schema SsvcPolicy |
Minimal
Only the fields the kind requires. From examples/SsvcPolicy/minimal.yaml.
YAML
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/SsvcPolicy.schema.json
apiVersion: vulnetix.com/v1
kind: SsvcPolicy
metadata:
name: default
spec: {}
HCL (resource vulnetix_ssvc_policy)
# Only the arguments the kind requires. From config-schemas examples/SsvcPolicy/minimal.yaml.
resource "vulnetix_ssvc_policy" "default" {
}
CLI (applies it)
export VULNETIX_CONFIG_TOKEN=vmt_... # a config management token
vulnetix config apply -f - --manager gitops:acme/platform --auto-approve <<'EOF'
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/SsvcPolicy.schema.json
apiVersion: vulnetix.com/v1
kind: SsvcPolicy
metadata:
name: default
spec: {}
EOF
Advanced
Every field the kind supports, with a when expression where it takes one. From examples/SsvcPolicy/advanced.yaml.
YAML
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/SsvcPolicy.schema.json
apiVersion: vulnetix.com/v1
kind: SsvcPolicy
metadata:
name: default
labels:
team: platform-security
annotations:
ticket: https://tracker.example.com/SEC-3120
spec:
# How Vulnetix's signals become the four decision points. These are the
# defaults written out, with a stricter automatable test.
exploitation:
active: kev || exploitMaturity in ['active', 'weaponized']
poc: exploitMaturity == 'poc' || exploitCount > 0
automatable: cvss >= 9.0 || epss >= 0.5
technicalImpact: cvss >= 9.0 || malicious
# Mission and well-being, from the repository's criticality.
missionWellBeing:
low: low
medium: medium
high: high
critical: high
unclassified: medium
# Cells of the CISA table the organisation reads differently.
mapping:
- exploitation: [active]
automatable: [no, yes]
technicalImpact: [total]
missionWellBeing: [medium, high]
outcome: Act
# Decide by rule, first match wins.
overrides:
- name: card data in scope
when: labels['pci'] == 'in-scope' && severity in ['critical', 'high']
outcome: Act
- name: no fix and not reachable
when: "!reachable && fixAvailability == 'no_fix'"
outcome: Track
outcomes:
track:
priority: low
dueDays: 90
trackStar:
priority: medium
dueDays: 60
attend:
priority: high
dueDays: 14
act:
priority: immediate
dueDays: 3
HCL (resource vulnetix_ssvc_policy)
# Every argument the kind supports. From config-schemas examples/SsvcPolicy/advanced.yaml.
resource "vulnetix_ssvc_policy" "default" {
automatable = "cvss >= 9.0 || epss >= 0.5"
exploitation = {
active = "kev || exploitMaturity in ['active', 'weaponized']"
poc = "exploitMaturity == 'poc' || exploitCount > 0"
}
mapping = [
{
automatable = ["no", "yes"]
exploitation = ["active"]
mission_well_being = ["medium", "high"]
outcome = "Act"
technical_impact = ["total"]
},
]
mission_well_being = {
critical = "high"
high = "high"
low = "low"
medium = "medium"
unclassified = "medium"
}
outcomes = {
act = {
due_days = 3
priority = "immediate"
}
attend = {
due_days = 14
priority = "high"
}
track = {
due_days = 90
priority = "low"
}
track_star = {
due_days = 60
priority = "medium"
}
}
overrides = [
{
name = "card data in scope"
outcome = "Act"
when = "labels['pci'] == 'in-scope' && severity in ['critical', 'high']"
},
{
name = "no fix and not reachable"
outcome = "Track"
when = "!reachable && fixAvailability == 'no_fix'"
},
]
technical_impact = "cvss >= 9.0 || malicious"
labels = {
team = "platform-security"
}
annotations = {
ticket = "https://tracker.example.com/SEC-3120"
}
}
CLI (applies it)
export VULNETIX_CONFIG_TOKEN=vmt_... # a config management token
vulnetix config apply -f - --manager gitops:acme/platform --auto-approve <<'EOF'
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/SsvcPolicy.schema.json
apiVersion: vulnetix.com/v1
kind: SsvcPolicy
metadata:
name: default
labels:
team: platform-security
annotations:
ticket: https://tracker.example.com/SEC-3120
spec:
# How Vulnetix's signals become the four decision points. These are the
# defaults written out, with a stricter automatable test.
exploitation:
active: kev || exploitMaturity in ['active', 'weaponized']
poc: exploitMaturity == 'poc' || exploitCount > 0
automatable: cvss >= 9.0 || epss >= 0.5
technicalImpact: cvss >= 9.0 || malicious
# Mission and well-being, from the repository's criticality.
missionWellBeing:
low: low
medium: medium
high: high
critical: high
unclassified: medium
# Cells of the CISA table the organisation reads differently.
mapping:
- exploitation: [active]
automatable: [no, yes]
technicalImpact: [total]
missionWellBeing: [medium, high]
outcome: Act
# Decide by rule, first match wins.
overrides:
- name: card data in scope
when: labels['pci'] == 'in-scope' && severity in ['critical', 'high']
outcome: Act
- name: no fix and not reachable
when: "!reachable && fixAvailability == 'no_fix'"
outcome: Track
outcomes:
track:
priority: low
dueDays: 90
trackStar:
priority: medium
dueDays: 60
attend:
priority: high
dueDays: 14
act:
priority: immediate
dueDays: 3
EOF
Fields
Paths use dots for nesting and [] for the items of a list. Required means required whenever the parent object is present. A default is what the server uses when the field is left out.
| Field | Type | Required | Default | Allowed values | Description |
|---|---|---|---|---|---|
apiVersion |
string | yes | exactly vulnetix.com/v1 |
The document format version. Every kind in this release is vulnetix.com/v1. | |
kind |
string | yes | exactly SsvcPolicy |
Always SsvcPolicy. | |
metadata |
object | yes | no other keys | Metadata for a kind the tenant has exactly one of. The name is fixed to default. | |
metadata.name |
string | yes | exactly default; pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ |
The object’s name, unique per kind within a tenant. It is the natural key: plan, apply, export and the Terraform provider all address an object by kind and name. | |
metadata.org |
string | no | format uuid |
The tenant (organization UUID) the document belongs to. When set, plan and apply refuse to run against any other tenant. | |
metadata.labels |
object | no | map of string to string (length 0 to 256); keys match ^[a-z0-9]([a-z0-9._/-]{0,61}[a-z0-9])?$ |
Free key/value labels. Stored with the object, never interpreted by the server. | |
metadata.annotations |
object | no | map of string to string (length 0 to 4096) | Free key/value notes for people and tools, such as a ticket link. | |
spec |
object | yes | no other keys | ||
spec.exploitation |
object | no | no other keys | When a finding counts as exploited. Active wins when both conditions hold; when neither does, exploitation is none. | |
spec.exploitation.active |
string | no | `kev | exploitMaturity in [‘active’, ‘weaponized’]` | |
spec.exploitation.poc |
string | no | `exploitMaturity == ‘poc’ | exploitCount > 0` | |
spec.automatable |
string | no | `cvss >= 9.0 | epss >= 0.7` | |
spec.technicalImpact |
string | no | `cvss >= 9.0 | malicious` | |
spec.missionWellBeing |
object | no | no other keys | The mission and well-being value of a finding in a repository, by the repository’s criticality (RepositorySettings spec.criticality). unclassified is a repository with no criticality. | |
spec.missionWellBeing.low |
string | no | low |
low, medium, high |
Mission and well-being: the combination of mission prevalence and public well-being impact. |
spec.missionWellBeing.medium |
string | no | medium |
low, medium, high |
Mission and well-being: the combination of mission prevalence and public well-being impact. |
spec.missionWellBeing.high |
string | no | high |
low, medium, high |
Mission and well-being: the combination of mission prevalence and public well-being impact. |
spec.missionWellBeing.critical |
string | no | high |
low, medium, high |
Mission and well-being: the combination of mission prevalence and public well-being impact. |
spec.missionWellBeing.unclassified |
string | no | medium |
low, medium, high |
Mission and well-being: the combination of mission prevalence and public well-being impact. |
spec.mapping |
array of object | no | 0 to 36 items | Reads cells of the decision table differently. A rule matches every cell whose decision points are among the values it lists; a decision point it leaves out matches any value. When several rules match a cell, the last one wins. A rule changes a cell for the whole tenant: to decide by a property of the finding or its repository, use overrides. | |
spec.mapping[].exploitation |
array of string | no | 1 to any items; items unique; each item: none, poc, active |
||
spec.mapping[].automatable |
array of string | no | 1 to any items; items unique; each item: no, yes |
||
spec.mapping[].technicalImpact |
array of string | no | 1 to any items; items unique; each item: partial, total |
||
spec.mapping[].missionWellBeing |
array of string | no | 1 to any items; items unique; each item: low, medium, high |
||
spec.mapping[].outcome |
string | yes | Track, Track*, Attend, Act |
The SSVC decision. Track and Track* are remediated in the standard update cycle (Track* is watched more closely), Attend sooner, Act as soon as possible. | |
spec.overrides |
array of object | no | 0 to 50 items | Decides the findings a condition holds for by rule, after the table and the mapping. The first rule that holds sets the outcome, so list the most specific first. | |
spec.overrides[].name |
string | no | length 0 to 100 | Shown beside the decision when this rule set it. | |
spec.overrides[].when |
string | yes | length 1 to 4096 | CEL over the finding variables, including the repository’s labels, owner and criticality. | |
spec.overrides[].outcome |
string | yes | Track, Track*, Attend, Act |
The SSVC decision. Track and Track* are remediated in the standard update cycle (Track* is watched more closely), Attend sooner, Act as soon as possible. | |
spec.outcomes |
object | no | no other keys | What each outcome asks of the organisation. The priority is what a finding with that outcome is ranked as; dueDays is its remediation window, and 0 or left out means the standard update cycle. | |
spec.outcomes.track |
object | no | no other keys | ||
spec.outcomes.track.priority |
string | no | low, medium, high, immediate |
low, medium, high or immediate. Defaults: Track low, Track* medium, Attend high, Act immediate. | |
spec.outcomes.track.dueDays |
integer | no | 0 |
0 to 365 | The remediation window in days. 0 or left out is the standard update cycle. |
spec.outcomes.trackStar |
object | no | no other keys | ||
spec.outcomes.trackStar.priority |
string | no | low, medium, high, immediate |
low, medium, high or immediate. Defaults: Track low, Track* medium, Attend high, Act immediate. | |
spec.outcomes.trackStar.dueDays |
integer | no | 0 |
0 to 365 | The remediation window in days. 0 or left out is the standard update cycle. |
spec.outcomes.attend |
object | no | no other keys | ||
spec.outcomes.attend.priority |
string | no | low, medium, high, immediate |
low, medium, high or immediate. Defaults: Track low, Track* medium, Attend high, Act immediate. | |
spec.outcomes.attend.dueDays |
integer | no | 0 |
0 to 365 | The remediation window in days. 0 or left out is the standard update cycle. |
spec.outcomes.act |
object | no | no other keys | ||
spec.outcomes.act.priority |
string | no | low, medium, high, immediate |
low, medium, high or immediate. Defaults: Track low, Track* medium, Attend high, Act immediate. | |
spec.outcomes.act.dueDays |
integer | no | 0 |
0 to 365 | The remediation window in days. 0 or left out is the standard update cycle. |
CEL variables
spec.exploitation.active, spec.exploitation.poc, spec.automatable, spec.technicalImpact and spec.overrides[].when are CEL expressions in the finding context, returning bool. It is evaluated by the SSVC decision in the Vulnetix site API, on every decision and summary (the summary covers at most 5000 open findings, most severe first, cached for 60 seconds), and by the VDB API for the CLI scan and the remediation plan. The decision point takes its lower value and the decision is still made. An override that fails does not hold. See where each expression is evaluated. The functions, limits and error codes are on CEL rule expressions.
| Variable | Type | Bound | Description |
|---|---|---|---|
id |
string |
256 chars | The advisory identifier, such as CVE-2024-3094 or a GHSA ID. |
aliases |
list(string) |
64 items of 256 chars | Other identifiers the advisory is known by. |
severity |
string |
64 chars | Lower case: critical, high, medium, low, or empty when unknown. |
cvss |
double |
CVSS base score, 0 to 10. 0 when unscored. | |
epss |
double |
EPSS probability, 0 to 1. | |
epssPercentile |
double |
EPSS percentile, 0 to 1. | |
kev |
bool |
Listed in any known-exploited catalogue: CISA KEV, VulnCheck KEV or EU KEV. | |
exploitMaturity |
string |
64 chars | The highest exploit tier, as the CLI --exploits gate grades it: poc, active, weaponized, or empty when none is known. |
exploitCount |
int |
Number of public exploits known. | |
reachability |
string |
64 chars | Lower case reachability verdict: direct, transitive, semantic, unreachable, or empty when not assessed. |
reachable |
bool |
Derived. True when reachability is direct, transitive or semantic, the same set the findings page counts as reachable. |
|
internetExposed |
bool |
The package is observed on internet-facing infrastructure. | |
eol |
bool |
The package version is end of life. | |
malicious |
bool |
The package is known malware. | |
fixAvailability |
string |
64 chars | available, partial, no_fix, or empty when unknown. |
direct |
bool |
The package is a direct dependency, not a transitive one. | |
purl |
string |
2048 chars | Package URL of the affected package, with version. |
ecosystem |
string |
64 chars | Package ecosystem, such as npm, pypi, golang, maven. |
cwe |
list(string) |
64 items of 64 chars | CWE identifiers, upper case with prefix: CWE-79. |
repo |
string |
512 chars | Repository full name, owner/name. |
branch |
string |
512 chars | Branch the finding was scanned on. |
path |
string |
1024 chars | File the finding came from, such as the manifest or lock file, relative to the repository root. |
labels |
map(string, string) |
50 keys, keys and values of 256 chars | The key/value labels of the repository the finding is in, as set on the repository’s asset record or in a RepositorySettings document: labels['tier'] == 'prod'. Empty when the repository has none, and in a runtime that does not know the repository’s asset record, so 'tier' in labels is false there. |
owner |
string |
512 chars | Who owns the repository: a team name, a user name or an email address, as set on its asset record. Empty when none is set. |
criticality |
string |
64 chars | How much the business depends on the repository: low, medium, high or critical. Empty when none is set, so criticality in ['high', 'critical'] is false for an unclassified repository. |
now |
timestamp |
The time of evaluation. Callers may fix it; otherwise it is the clock when Eval runs. |
Applying this kind
- Command.
vulnetix config apply -f <file>; the CLI line above says from which release. Plan first withvulnetix config plan: it shows each object as create, update, delete or no change, with aplanHashthat apply then holds to. Apply writes in one transaction. - Manager. Each object records the manager that applied it (
terraform,consoleorgitops:<repo>).--prunedeletes only objects its own manager owns. See The manager and the config engine. - Status. Applied, Unchanged or Deleted, with no other status fields. The phases the engine reports are explained under status phases.
- Singleton. There is one per tenant, and its name is always
default.
How the decision is made
Vulnetix makes the CISA SSVC decision, Track, Track*, Attend or Act, for a finding from four decision points. The decision table is the CISA Coordinator table 2.0.3 (CERT/CC) and is fixed. This kind configures how Vulnetix’s signals become the four values, which table cells the organization reads differently, which findings it decides by rule, and what each outcome asks of it.
| Decision point | Values | Default |
|---|---|---|
| Exploitation | none, poc, active | active: kev || exploitMaturity in ['active', 'weaponized']. poc: exploitMaturity == 'poc' || exploitCount > 0. Active wins over poc. |
| Automatable | no, yes | cvss >= 9.0 || epss >= 0.7 |
| Technical impact | partial, total | total: cvss >= 9.0 || malicious |
| Mission and well-being | low, medium, high | The repository’s criticality: low, medium, high, and critical (high). A repository with no criticality is medium. |
The expressions are CEL over the finding context, so a policy can use any finding variable, including the repository’s labels, owner and criticality. The defaults approximate CISA’s definitions from the signals Vulnetix holds. Automatable in particular is defined by the kill chain, and the finding context carries no CVSS vector, so the default reads a critical score or a high EPSS as automatable. A tenant that holds better information tightens the expression. mapping[] reads cells of the table differently for the whole tenant (the last matching rule wins), overrides[] decides by rule after the table and the mapping (the first match wins, and the decision records which rule set it), and outcomes sets each outcome’s priority and dueDays (0 is the standard update cycle).
How apply treats it
- A singleton, named
default. The engine stores only what differs from the default policy, so a document that spells a default reads back without it. - Plan compiles every expression against the
findingcontext and refuses unknown values. A stored policy that no longer compiles is not applied: the tenant decides with the default and the routes report awarning. - Delete deletes the row, which returns the tenant to the default policy.
- It needs saas migration
20261228010000_ssvc_policy. Until it runs, a tenant decides with the default policy, and a write or an apply is refused naming the migration. - Decisions are made when they are read, from the same facts as the risk ranking, with the repository’s asset record. Nothing is stored per finding, so a policy change shows at once. The routes are
GET,PUTandDELETE /v1/ssvc/policy,POST /v1/ssvc/decideandGET /v1/ssvc/summary.
In the CLI scan and the remediation plan
The VDB API decides with this policy in two more places: the CLI’s SCA response (POST /v2/cli/sca, the vulnetix:ssvc property of each vulnerability) and the remediation plan (GET /v2/vuln/{id}/remediation-plan, response.ssvc, which takes an optional ?repo=owner/name). Mission and well-being come from the repository’s criticality, and a repository with no asset record is decided as medium.
The policy applies only when the tenant has stored one. With no stored policy, the community credential, a database without the SsvcPolicy table, or a stored policy that no longer compiles, both keep the decision they made before policies existed, which is simpler and ignores criticality. A decision made under a policy adds vulnetix:ssvc:priority, :source, :exploitation, :automatable, :technicalImpact, :missionWellBeing, and :rule and :dueDays when they apply, to a scanned vulnerability, and policy, missionWellBeing, source, rule and dueDays to the plan’s ssvc block. The fields that existed keep their meaning.
Terraform and OpenTofu
The resource is vulnetix_ssvc_policy in the vulnetix/resolve provider. Its arguments are the spec in snake case, beside name, labels and annotations from metadata. The Minimal and Advanced HCL are above. The provider’s own reference is vulnetix_ssvc_policy.
Import an object that already exists by its name (default):
# Import an existing SsvcPolicy by its name (metadata.name).
terraform import vulnetix_ssvc_policy.default default
Install, authentication and how state is read: Terraform and OpenTofu.
Check a file with vulnetix config validate -f <file>. See Validate documents. In an editor, start the file with the schema comment.