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:
- changing
confidence from high to low without supplying a new confidence_rationale;
- 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
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:
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:
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.
Summary
update_vulnerability_reportcan successfully transform a valid stored vulnerability report into a state thatcreate_vulnerability_reportwould 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:
confidencefromhightolowwithout supplying a newconfidence_rationale;cvss_breakdownso thatseveritychanges, without supplying newseverity_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_updatestrix/report/state.pyReportState.update_vulnerability_report_DEPENDENT_REPORT_FIELDSVerified against the current
mainimplementation on 2026-09-12.Case 1 — confidence can become invalid after update
The creation path requires a rationale whenever confidence is not
high.Conceptually:
is rejected when creating a report.
However, consider an existing valid report:
Then perform a partial update:
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:
This is a state that the creation validator itself considers invalid.
Expected
The update should fail unless a valid
confidence_rationaleaccompanies a transition tomediumorlow.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:
Now update only
cvss_breakdown.The update code recalculates:
from the new vector.
If the new vector changes severity from
mediumtohigh,ReportState._DEPENDENT_REPORT_FIELDScorrectly recognizes the previousseverity_change_conditionsas stale and removes it when no replacement is supplied.The resulting report can therefore become:
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:
The missing step is validation of the final candidate state:
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:
But removing stale reasoning can itself create a new obligation:
That second condition is not currently enforced for these dynamic-finding fields.
Suggested regression tests
At minimum:
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:
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.