Shaun Seidman
Engineering Manager · Denver, CO
GitHub ↗LinkedIn ↗

03 / note · September 24, 2026

Using Jev to judge CVE risk exceptions

What I learned by building a security workflow around typed judgments, sourced evidence, and one deliberately boring if statement.

I jumped onto the Jev hype train, see: Jev Explained: Demos and Use Cases.

I built a CVE tracker. The tool helps with two questions: can we accept the risk of delaying this patch, and what do we know about resolving it?

Jev does not generate text. You send it some state and one or more typed questions. It returns structured judgments that code can use without parsing a paragraph first. A CVE review gave me a useful case because the answers could already take the form of classifications and scores.

One question was doing too much

Jev has three primitives: Choice, Score, and Noul. Choice picks from a set of named options. Score places the state along an ordered rubric. Noul returns the probability that a statement is true.

The constraint helped. A risk exception can turn into a long discussion full of generic assurances. I had to break that discussion into questions with answers my code could use:

  • Does the justification address this vulnerability's exploit path?
  • Does it name a real compensating control?
  • How much residual risk remains if the team delays the patch?

The first two are Noul questions. The last is a Score with four levels, from "patch immediately" to "acceptable to defer on the normal patch cycle."

const questions = {
  justification_addresses_exploit: {
    type: "noul",
    instructions:
      "Does the justification specifically address this CVE's actual attack vector or affected component?",
  },
  compensating_control: {
    type: "noul",
    instructions:
      "Does the justification propose a concrete compensating control?",
  },
  decision_gate: {
    type: "score",
    instructions:
      "How much residual risk would remain if this patch were deferred?",
    criteria: RISK_GATE_LEVELS,
  },
};

I send all three against the same state in one request. The response already contains numbers, selected levels, probability distributions, and confidence. The server does not have to ask a model for JSON and hope the JSON survives contact with a code fence.

Writing those questions exposed vague parts of my own thinking. "Is this safe?" is a rotten question because safe has no useful boundary. A justification can mention the affected component while offering no control that changes the risk. Splitting the judgment made that difference visible in the result.

The CVE data comes first

A security tool cannot improvise the facts and then grade its own work. Before the app calls Jev, the server fetches the CVE description and CVSS data from the National Vulnerability Database. It also checks CISA's Known Exploited Vulnerabilities catalog for evidence of active exploitation, required actions, and due dates.

The state sent to Jev looks roughly like this:

const state = {
  cve_id: cve.id,
  cve_description: cve.description,
  cvss_score: cve.cvssScore,
  cvss_severity: cve.cvssSeverity,
  actively_exploited_per_cisa_kev: Boolean(kevEntry),
  cisa_kev_note: kevEntry
    ? `Added to CISA KEV on ${kevEntry.dateAdded}`
    : "Not currently listed in CISA KEV.",
  justification,
};

That division made the rest of the project click. NVD and CISA supply the facts. The person supplies the reason for deferring the patch. Jev judges the case they form together.

The remediation side follows the same rule. The app shows CISA's required action and useful NVD references as source material. Jev classifies the remediation as a patch, workaround, product removal, or no available fix, then scores how specific the guidance is. It does not invent a shell command or tell someone to install a version it made up.

I would rather show a thin answer with links than polished remediation advice that has no source behind it.

CISA gets a hard override

I did not want a model score to overrule a known fact about active exploitation. If CISA has added a CVE to the KEV catalog and Jev still leans toward accepting the delay, the server raises a contradiction warning.

let contradiction = null;

if (kevEntry && gateScore !== null && gateScore >= gateMax * 0.75) {
  contradiction =
    `CISA lists ${cveId} as actively exploited, ` +
    "but the decision gate supports delaying the patch.";
}

That check is deliberately boring. Jev can weigh the quality of a justification. The application owns the policy that a confirmed external signal should block an easy acceptance.

This was also where confidence became more useful to me. A confidence value describes how concentrated the probability distribution is. It tells the interface whether Jev saw a clear answer or a close call. Confidence does not make a high-risk action safe, and it does not replace the CISA check. It gives the person reviewing the result another reason to slow down.

The final card calls itself a decision record and still says that a person makes the decision. I wanted the output to support a security review, not cosplay as the security reviewer.

One CVE can need two records

My first version used the CVE ID as the key for saved items. Marking a CVE as resolved after accepting its risk overwrote the earlier record.

That looked reasonable in the UI and was wrong in a way that matters. A team may accept risk on Monday, add a compensating control, and patch the system on Friday. The accepted exception and the resolution describe two events in the history of the same vulnerability.

I changed the key to (cveId, status). One CVE can now keep an Accepted record with its original justification and a Resolved record with the guidance used later. The storage is still localStorage because I did not need to invent a user system to learn about typed judgments.

Splitting the form followed the same reasoning. Looking for a fix only needs a CVE ID. Accepting risk needs a justification. The early UI put both actions under one form, which made the remediation path look like it had forgotten a required field. They now sit in separate blocks and share only the CVE ID.

Jev still needs code around it

The CVE tracker gave Jev's constraint a reason to exist.

I had real state from sources I could name. I had small judgments that fit Choice, Score, and Noul. I had application rules that should stay deterministic. I also had a person who needed to see the evidence before acting.

I came away from the project less interested in giving a model the whole problem. Jev has a narrow job here, and I can show what it judged beside the sources it used. When policy needs to disagree, a plain if statement still gets to do that.