17 KiB
Here’s a crisp, ready‑to‑use rule for VEX hygiene that will save you pain in audits and customer reviews—and make Stella Ops look rock‑solid.
Adopt a strict “not_affected only with proof” policy
What it means (plain English):
Only mark a vulnerability as not_affected if you can prove the vulnerable code can’t run in your product under defined conditions—then record that proof (scope, entry points, limits) inside a VEX bundle.
The non‑negotiables
-
Audit coverage: You must enumerate the reachable entry points you audited (e.g., exported handlers, CLI verbs, HTTP routes, scheduled jobs, init hooks). State their limits (versions, build flags, feature toggles, container args, config profiles).
-
VEX justification required: Use a concrete justification (OpenVEX/CISA style), e.g.:
vulnerable_code_not_in_execute_pathcomponent_not_presentvulnerable_code_cannot_be_controlled_by_adversaryinline_mitigation_already_in_place
-
Impact or constraint statement: Explain why it’s safe given your product’s execution model: sandboxing, dead code elimination, policy blocks, feature gates, OS hardening, container seccomp/AppArmor, etc.
-
VEX proof bundle: Store the evidence alongside the VEX: call‑graph slices, reachability reports, config snapshots, build args, lattice/policy decisions, test traces, and hashes of the exact artifacts (SBOM + attestation refs). This is what makes the claim stand up in an audit six months later.
Minimal OpenVEX example (drop‑in)
{
"document": {
"id": "urn:stellaops:vex:2025-11-25:svc-api:log4j:2.14.1",
"author": "Stella Ops Authority",
"role": "vex"
},
"statements": [
{
"vulnerability": "CVE-2021-44228",
"products": ["pkg:maven/com.acme/svc-api@1.7.3?type=jar"],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "Log4j JNDI classes excluded at build; no logger bridge; JVM flags `-Dlog4j2.formatMsgNoLookups=true` enforced by container entrypoint.",
"analysis": {
"entry_points_audited": [
"com.acme.api.HttpServer#routes",
"com.acme.jobs.Cron#run",
"Main#init"
],
"limits": {
"image_digest": "sha256:…",
"config_profile": "prod",
"args": ["--no-dynamic-plugins"],
"seccomp": "stellaops-baseline-v3"
},
"evidence_refs": [
"dsse:sha256:…/reachability.json",
"dsse:sha256:…/build-args.att",
"dsse:sha256:…/policy-lattice.proof"
]
},
"timestamp": "2025-11-25T00:00:00Z"
}
]
}
Fast checklist (use this on every not_affected)
- Define product + artifact by immutable IDs (PURL + digest).
- List audited entry points and execution limits.
- Declare status =
not_affectedwith a justification from the allowed set. - Add a short impact/why‑safe sentence.
- Attach evidence: call graph, configs, policies, build args, test traces.
- Sign the VEX (DSSE/In‑Toto), link it to the SBOM attestation.
- Version and keep the proof bundle with your release.
When to use an exception (temporary VEX)
If you can prove non‑reachability only under a temporary constraint (e.g., feature flag off while a permanent fix lands), emit a time‑boxed exception VEX:
- Add
constraints.expiresand the required control (e.g.,feature_flag=Off,policy=BlockJNDI). - Schedule an auto‑recheck on expiry; flip to
affectedif the constraint lapses.
If you want, I can generate a Stella Ops‑flavored VEX template and a tiny “proof bundle” schema (JSON) so your devs can drop it into the pipeline and your documentators can copy‑paste the rationale blocks. Cool, let’s turn that policy into something your devs can actually follow day‑to‑day.
Below is a concrete implementation plan you can drop into an internal RFC / Notion page and wire into your pipelines.
0. What we’re implementing (for context)
Goal: At Stella Ops, you can only mark a vulnerability as not_affected if:
- You’ve audited specific entry points under clearly documented limits (version, build flags, config, container image).
- You’ve captured evidence and rationale in a VEX statement + proof bundle.
- The VEX is validated, signed, and shipped with the artifact.
We’ll standardize on OpenVEX with a small extension (analysis section) for developer‑friendly evidence.
1. Repo & artifact layout (week 1)
1.1. Create a standard security layout
In each service repo:
/security/
vex/
openvex.json # aggregate VEX doc (generated/curated)
statements/ # one file per CVE (optional, if you like)
proofs/
CVE-YYYY-NNNN/
reachability.json
configs/
tests/
notes.md
schemas/
openvex.schema.json # JSON schema with Stella extensions
Developer guidance:
- If you touch anything related to a vulnerability decision, you edit
security/vex/andsecurity/proofs/in the same PR.
2. Define the VEX schema & allowed justifications (week 1)
2.1. Fix the format & fields
You’ve already chosen OpenVEX, so formalize the required extras:
{
"vulnerability": "CVE-2021-44228",
"products": ["pkg:maven/com.acme/svc-api@1.7.3?type=jar"],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "…",
"analysis": {
"entry_points_audited": [
"com.acme.api.HttpServer#routes",
"com.acme.jobs.Cron#run",
"Main#init"
],
"limits": {
"image_digest": "sha256:…",
"config_profile": "prod",
"args": ["--no-dynamic-plugins"],
"seccomp": "stellaops-baseline-v3"
},
"evidence_refs": [
"dsse:sha256:…/reachability.json",
"dsse:sha256:…/build-args.att",
"dsse:sha256:…/policy-lattice.proof"
]
}
}
Action items:
-
Write a JSON schema for the
analysisblock (required fornot_affected):entry_points_audited: non‑empty array of strings.limits: object with at least one ofimage_digest,config_profile,args,seccomp,feature_flags.evidence_refs: non‑empty array of strings.
-
Commit this as
security/schemas/openvex.schema.json.
2.2. Fix the allowed justification values
Publish an internal list, e.g.:
vulnerable_code_not_in_execute_pathcomponent_not_presentvulnerable_code_cannot_be_controlled_by_adversaryinline_mitigation_already_in_placeprotected_by_environment(e.g., mandatory sandbox, read‑only FS)
Rule: any not_affected must pick one of these. Any new justification needs security team approval.
3. Developer process for handling a new vuln (week 2)
This is the “how to act” guide devs follow when a CVE pops up in scanners or customer reports.
3.1. Decision flow
-
Is the vulnerable component actually present?
- If no →
status: not_affected,justification: component_not_present. Still fill outproducts,impact_statement(explain why it’s not present: different version, module excluded, etc.).
- If no →
-
If present: analyze reachability.
-
Identify entry points of the service:
- HTTP routes, gRPC methods, message consumers, CLI commands, cron jobs, startup hooks.
-
Check:
- Is the vulnerable path reachable from any of these?
- Is it blocked by configuration / feature flags / sandboxing?
-
-
If reachable or unclear → treat as
affected.- Plan a patch, workaround, or runtime mitigation.
-
If not reachable & you can argue that clearly →
not_affectedwith proof.-
Fill in:
entry_points_auditedlimitsevidence_refsimpact_statement(“why safe”)
-
3.2. Developer checklist (drop this into your docs)
Stella Ops
not_affectedchecklistFor any CVE you mark as
not_affected:
Identify product + artifact
- PURL (package URL)
- Image digest / binary hash
Audit execution
- List entry points you reviewed
- Note the limits (config profile, feature flags, container args, sandbox)
Collect evidence
- Reachability analysis (manual or tool report)
- Config snapshot (YAML, env vars, Helm values)
- Tests or traces (if applicable)
Write VEX statement
status = not_affectedjustificationfrom allowed listimpact_statementexplains “why safe”analysis.entry_points_audited,analysis.limits,analysis.evidence_refsWire into repo
- Proofs stored under
security/proofs/CVE-…/- VEX updated under
security/vex/Request review
- Security reviewer approved in PR
4. Automation & tooling for devs (week 2–3)
Make it easy to “do the right thing” with a small CLI and CI jobs.
4.1. Add a small vexctl helper
Language doesn’t matter—Python is fine. Rough sketch:
#!/usr/bin/env python3
import json
from pathlib import Path
from datetime import datetime
VEX_PATH = Path("security/vex/openvex.json")
def load_vex():
if VEX_PATH.exists():
return json.loads(VEX_PATH.read_text())
return {"document": {}, "statements": []}
def save_vex(data):
VEX_PATH.write_text(json.dumps(data, indent=2, sort_keys=True))
def add_statement():
cve = input("CVE ID (e.g. CVE-2025-1234): ").strip()
product = input("Product PURL: ").strip()
status = input("Status [affected/not_affected/fixed]: ").strip()
justification = None
analysis = None
if status == "not_affected":
justification = input("Justification (from allowed list): ").strip()
entry_points = input("Entry points (comma-separated): ").split(",")
limits_profile = input("Config profile (e.g. prod/stage): ").strip()
image_digest = input("Image digest (optional): ").strip()
evidence = input("Evidence refs (comma-separated): ").split(",")
analysis = {
"entry_points_audited": [e.strip() for e in entry_points if e.strip()],
"limits": {
"config_profile": limits_profile or None,
"image_digest": image_digest or None
},
"evidence_refs": [e.strip() for e in evidence if e.strip()]
}
impact = input("Impact / why safe (short text): ").strip()
vex = load_vex()
vex.setdefault("document", {})
vex.setdefault("statements", [])
stmt = {
"vulnerability": cve,
"products": [product],
"status": status,
"impact_statement": impact,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
if justification:
stmt["justification"] = justification
if analysis:
stmt["analysis"] = analysis
vex["statements"].append(stmt)
save_vex(vex)
print(f"Added VEX statement for {cve}")
if __name__ == "__main__":
add_statement()
Dev UX: run:
./tools/vexctl add
and follow prompts instead of hand‑editing JSON.
4.2. Schema validation in CI
Add a CI job (GitHub Actions example) that:
-
Installs
jsonschema. -
Validates
security/vex/openvex.jsonagainstsecurity/schemas/openvex.schema.json. -
Fails if:
- any
not_affectedstatement lacksanalysis.*fields, or justificationis not in the allowed list.
- any
name: VEX validation
on:
pull_request:
paths:
- "security/vex/**"
- "security/schemas/**"
jobs:
validate-vex:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install deps
run: pip install jsonschema
- name: Validate OpenVEX
run: |
python tools/validate_vex.py
Example validate_vex.py core logic:
import json
from jsonschema import validate, ValidationError
from pathlib import Path
import sys
schema = json.loads(Path("security/schemas/openvex.schema.json").read_text())
vex = json.loads(Path("security/vex/openvex.json").read_text())
try:
validate(instance=vex, schema=schema)
except ValidationError as e:
print("VEX schema validation failed:", e, file=sys.stderr)
sys.exit(1)
ALLOWED_JUSTIFICATIONS = {
"vulnerable_code_not_in_execute_path",
"component_not_present",
"vulnerable_code_cannot_be_controlled_by_adversary",
"inline_mitigation_already_in_place",
"protected_by_environment",
}
for stmt in vex.get("statements", []):
if stmt.get("status") == "not_affected":
just = stmt.get("justification")
if just not in ALLOWED_JUSTIFICATIONS:
print(f"Invalid justification '{just}' in statement {stmt.get('vulnerability')}")
sys.exit(1)
analysis = stmt.get("analysis") or {}
missing = []
if not analysis.get("entry_points_audited"):
missing.append("analysis.entry_points_audited")
if not analysis.get("limits"):
missing.append("analysis.limits")
if not analysis.get("evidence_refs"):
missing.append("analysis.evidence_refs")
if missing:
print(
f"'not_affected' for {stmt.get('vulnerability')} missing fields: {', '.join(missing)}"
)
sys.exit(1)
5. Signing & publishing VEX + proof bundles (week 3)
5.1. Signing
Pick a signing mechanism (e.g., DSSE + cosign/in‑toto), but keep the dev‑visible rules simple:
-
CI step:
-
Build artifact (image/binary).
-
Generate/update SBOM.
-
Validate VEX.
-
Sign:
- The artifact.
- The SBOM.
- The VEX document.
-
Enforce KMS‑backed keys controlled by the security team.
5.2. Publishing layout
Decide a canonical layout in your artifact registry / S3:
artifacts/
svc-api/
1.7.3/
image.tar
sbom.spdx.json
vex.openvex.json
proofs/
CVE-2025-1234/
reachability.json
configs/
tests/
Link evidence by digest (evidence_refs) so you can prove exactly what you audited.
6. PR / review policy (week 3–4)
6.1. Add a PR checklist item
In your PR template:
### Security / VEX
- [ ] If this PR **changes how we handle a known CVE** or marks one as `not_affected`, I have:
- [ ] Updated `security/vex/openvex.json`
- [ ] Added/updated proof bundle under `security/proofs/`
- [ ] Ran `./tools/vexctl` and CI VEX validation locally
6.2. Require security reviewer for not_affected changes
Add a CODEOWNERS entry:
/security/vex/* @stellaops-security-team
/security/proofs/* @stellaops-security-team
- Any PR touching these paths must be approved by security.
7. Handling temporary exceptions (time‑boxed VEX)
Sometimes you’re only safe because of a temporary constraint (e.g., feature flag off until patch). For those:
- Add a
constraintsblock:
"constraints": {
"control": "feature_flag",
"name": "ENABLE_UNSAFE_PLUGIN_API",
"required_value": "false",
"expires": "2025-12-31T23:59:59Z"
}
-
Add a scheduled job (e.g., weekly) that:
- Parses VEX.
- Finds any
constraints.expires < now(). - Opens an issue or fails a synthetic CI job: “Constraint expired: reevaluate CVE‑2025‑1234”.
Dev guidance: do not treat time‑boxed exceptions as permanent; they must be re‑reviewed or turned into affected + mitigation.
8. Rollout plan by week
You can present this timeline internally:
-
Week 1
- Finalize OpenVEX +
analysisschema. - Create
security/layout in 1–2 key services. - Publish allowed
justificationlist + written policy.
- Finalize OpenVEX +
-
Week 2
- Implement
vexctlhelper. - Add CI validation job.
- Pilot with one real CVE decision; walk through full proof bundle creation.
- Implement
-
Week 3
- Add signing + publishing steps for SBOM and VEX.
- Wire artifact registry layout, link VEX + proofs per release.
-
Week 4
-
Enforce CODEOWNERS + PR checklist across all services.
-
Enable scheduled checks for expiring constraints.
-
Run internal training (30–45 min) walking through:
- “Bad VEX” (hand‑wavy, no entry points) vs
- “Good VEX” (clear scope, evidence, limits).
-
9. What you can hand to devs right now
If you want, you can literally paste these as separate internal docs:
-
“How to mark a CVE as not_affected at Stella Ops”
- Copy section 3 (decision flow + checklist) and the VEX snippet.
-
“VEX technical reference for developers”
- Copy sections 1–2–4 (structure, schema, CLI, CI validation).
-
“VEX operations runbook”
- Copy sections 5–7 (signing, publishing, exceptions).
If you tell me which CI system you use (GitHub Actions, GitLab CI, Circle, etc.) and your primary stack (Java, Go, Node, etc.), I can turn this into exact job configs and maybe a more tailored vexctl CLI for your environment.