Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[BUG]Partial vulnerability report updates can violate create-time invariants #1299

Description

@hunter3x3-tech

Summary

update_vulnerability_report can successfully transform a valid stored vulnerability report into a state that create_vulnerability_report would reject as invalid.

The issue occurs because the update path validates the fields supplied in the revision, then merges those fields into the existing report and removes stale dependent fields, but does not revalidate the effective post-update report against the same cross-field invariants enforced during report creation.

Two deterministic examples are:

  1. changing confidence from high to low without supplying a new confidence_rationale;
  2. changing cvss_breakdown so that severity changes, without supplying new severity_change_conditions.

In both cases the update can succeed while producing a final report that violates the creation-time reporting contract.

Affected area

  • strix/tools/reporting/tool.py

    • _collect_update_changes
    • _do_update
  • strix/report/state.py

    • ReportState.update_vulnerability_report
    • _DEPENDENT_REPORT_FIELDS

Verified against the current main implementation on 2026-09-12.

Case 1 — confidence can become invalid after update

The creation path requires a rationale whenever confidence is not high.

Conceptually:

confidence = "low"
confidence_rationale = None

is rejected when creating a report.

However, consider an existing valid report:

confidence = high
confidence_rationale = absent

Then perform a partial update:

confidence = low

without supplying confidence_rationale.

The update validator verifies that "low" is a valid confidence enum value and accepts the field.

The state layer then merges the new confidence into the report.

The resulting report is:

confidence = low
confidence_rationale = absent

This is a state that the creation validator itself considers invalid.

Expected

The update should fail unless a valid confidence_rationale accompanies a transition to medium or low.

Actual

The partial update can succeed and persist a report that violates the create-time invariant.

Case 2 — severity conditions can disappear after CVSS revision

Creation also requires non-empty severity_change_conditions.

Consider a valid stored finding:

severity = medium
severity_change_conditions = "Raise to high if unauthenticated exploitation is confirmed."

Now update only cvss_breakdown.

The update code recalculates:

cvss
severity

from the new vector.

If the new vector changes severity from medium to high, ReportState._DEPENDENT_REPORT_FIELDS correctly recognizes the previous severity_change_conditions as stale and removes it when no replacement is supplied.

The resulting report can therefore become:

severity = high
severity_change_conditions = absent

Again, this is a state that report creation would reject.

Expected

If a revision causes severity to change, the effective revised report should still satisfy the requirement for severity_change_conditions.

The update should either require a replacement or reject the revision.

Actual

The stale dependent value is removed, but the resulting report is not revalidated for the now-missing required dependency.

Root cause

The update pipeline effectively performs:

validate(delta)
    ↓
derive implicit fields
    ↓
merge(existing, delta)
    ↓
remove stale dependencies
    ↓
persist

The missing step is validation of the final candidate state:

validate(delta)
    ↓
derive implicit fields
    ↓
construct candidate final report
    ↓
invalidate stale dependencies
    ↓
validate(candidate final report)
    ↓
persist

In other words, a valid original report plus a locally valid revision does not necessarily produce a valid final report.

Why this is easy to miss

The current dependent-field cleanup is useful and fixes one side of the consistency problem:

primary field changes
→ stale reasoning for the old value is removed

But removing stale reasoning can itself create a new obligation:

primary field changes
→ old dependent field removed
→ new state requires a replacement dependent field

That second condition is not currently enforced for these dynamic-finding fields.

Suggested regression tests

At minimum:

1. existing confidence=high, no rationale
   update confidence=low
   → reject

2. existing confidence=high
   update confidence=low + new confidence_rationale
   → accept

3. existing severity=medium + severity_change_conditions
   update cvss_breakdown so severity becomes high
   without new severity_change_conditions
   → reject

4. same CVSS update with new severity_change_conditions
   → accept

A broader regression would validate the complete effective report after every revision rather than adding individual checks only for these two examples.

Suggested implementation direction

Build the candidate merged report first, invalidate dependent fields whose basis changed, and then run the relevant cross-field invariants against the candidate before persistence.

Conceptually:

candidate = merge(existing, normalized_changes)
candidate = invalidate_superseded_dependencies(existing, candidate)

errors = validate_effective_report(candidate)

if errors:
    reject_update(errors)

persist(candidate)

This would preserve partial-update semantics while preventing a successful update from producing a state that the creation path considers invalid.

Related context

This behavior appears to originate from the explicit report revision path introduced in PR #1210.

That PR intentionally removes dependent reasoning when its primary field changes, which prevents stale reasoning from remaining attached to a new rating. The missing case is re-establishing dependencies that become mandatory in the resulting state.

Method attribution

Identified using ADECAS — Assured Decision, Evidence, Capability and Authority System.

Only the reproducible finding and suggested remediation are included here; internal ADECAS methods are not disclosed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions