Files
git.stella-ops.org/docs/product-advisories/25-Nov-2025 - Define Safe VEX 'Not Affected' Claims with Proofs.md
master e950474a77
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
up
2025-11-27 15:16:31 +02:00

17 KiB
Raw Blame History

Heres a crisp, readytouse rule for VEX hygiene that will save you pain in audits and customer reviews—and make StellaOps look rocksolid.

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 cant run in your product under defined conditions—then record that proof (scope, entry points, limits) inside a VEX bundle.

The nonnegotiables

  • 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_path
    • component_not_present
    • vulnerable_code_cannot_be_controlled_by_adversary
    • inline_mitigation_already_in_place
  • Impact or constraint statement: Explain why its safe given your products 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: callgraph 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 (dropin)

{
  "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_affected with a justification from the allowed set.
  • Add a short impact/whysafe sentence.
  • Attach evidence: call graph, configs, policies, build args, test traces.
  • Sign the VEX (DSSE/InToto), 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 nonreachability only under a temporary constraint (e.g., feature flag off while a permanent fix lands), emit a timeboxed exception VEX:

  • Add constraints.expires and the required control (e.g., feature_flag=Off, policy=BlockJNDI).
  • Schedule an autorecheck on expiry; flip to affected if the constraint lapses.

If you want, I can generate a StellaOpsflavored VEX template and a tiny “proof bundle” schema (JSON) so your devs can drop it into the pipeline and your documentators can copypaste the rationale blocks. Cool, lets turn that policy into something your devs can actually follow daytoday.

Below is a concrete implementation plan you can drop into an internal RFC / Notion page and wire into your pipelines.


0. What were implementing (for context)

Goal: At Stella Ops, you can only mark a vulnerability as not_affected if:

  1. Youve audited specific entry points under clearly documented limits (version, build flags, config, container image).
  2. Youve captured evidence and rationale in a VEX statement + proof bundle.
  3. The VEX is validated, signed, and shipped with the artifact.

Well standardize on OpenVEX with a small extension (analysis section) for developerfriendly 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/ and security/proofs/ in the same PR.

2. Define the VEX schema & allowed justifications (week 1)

2.1. Fix the format & fields

Youve 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 analysis block (required for not_affected):

    • entry_points_audited: nonempty array of strings.
    • limits: object with at least one of image_digest, config_profile, args, seccomp, feature_flags.
    • evidence_refs: nonempty 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_path
  • component_not_present
  • vulnerable_code_cannot_be_controlled_by_adversary
  • inline_mitigation_already_in_place
  • protected_by_environment (e.g., mandatory sandbox, readonly 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

  1. Is the vulnerable component actually present?

    • If no → status: not_affected, justification: component_not_present. Still fill out products, impact_statement (explain why its not present: different version, module excluded, etc.).
  2. 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?
  3. If reachable or unclear → treat as affected.

    • Plan a patch, workaround, or runtime mitigation.
  4. If not reachable & you can argue that clearly → not_affected with proof.

    • Fill in:

      • entry_points_audited
      • limits
      • evidence_refs
      • impact_statement (“why safe”)

3.2. Developer checklist (drop this into your docs)

Stella Ops not_affected checklist

For any CVE you mark as not_affected:

  1. Identify product + artifact

    • PURL (package URL)
    • Image digest / binary hash
  2. Audit execution

    • List entry points you reviewed
    • Note the limits (config profile, feature flags, container args, sandbox)
  3. Collect evidence

    • Reachability analysis (manual or tool report)
    • Config snapshot (YAML, env vars, Helm values)
    • Tests or traces (if applicable)
  4. Write VEX statement

    • status = not_affected
    • justification from allowed list
    • impact_statement explains “why safe”
    • analysis.entry_points_audited, analysis.limits, analysis.evidence_refs
  5. Wire into repo

    • Proofs stored under security/proofs/CVE-…/
    • VEX updated under security/vex/
  6. Request review

    • Security reviewer approved in PR

4. Automation & tooling for devs (week 23)

Make it easy to “do the right thing” with a small CLI and CI jobs.

4.1. Add a small vexctl helper

Language doesnt 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 handediting JSON.

4.2. Schema validation in CI

Add a CI job (GitHub Actions example) that:

  1. Installs jsonschema.

  2. Validates security/vex/openvex.json against security/schemas/openvex.schema.json.

  3. Fails if:

    • any not_affected statement lacks analysis.* fields, or
    • justification is not in the allowed list.
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/intoto), but keep the devvisible rules simple:

  • CI step:

    1. Build artifact (image/binary).

    2. Generate/update SBOM.

    3. Validate VEX.

    4. Sign:

      • The artifact.
      • The SBOM.
      • The VEX document.

Enforce KMSbacked 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 34)

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 (timeboxed VEX)

Sometimes youre only safe because of a temporary constraint (e.g., feature flag off until patch). For those:

  1. Add a constraints block:
"constraints": {
  "control": "feature_flag",
  "name": "ENABLE_UNSAFE_PLUGIN_API",
  "required_value": "false",
  "expires": "2025-12-31T23:59:59Z"
}
  1. 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 CVE20251234”.

Dev guidance: do not treat timeboxed exceptions as permanent; they must be rereviewed or turned into affected + mitigation.


8. Rollout plan by week

You can present this timeline internally:

  • Week 1

    • Finalize OpenVEX + analysis schema.
    • Create security/ layout in 12 key services.
    • Publish allowed justification list + written policy.
  • Week 2

    • Implement vexctl helper.
    • Add CI validation job.
    • Pilot with one real CVE decision; walk through full proof bundle creation.
  • 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 (3045 min) walking through:

      • “Bad VEX” (handwavy, 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 124 (structure, schema, CLI, CI validation).
  • “VEX operations runbook”

    • Copy sections 57 (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.