precogly / precogly/precogly

[Bug]: Gap, waived, and decommissioned controls retain stale residual-risk credit

Open
#537 0 comments 0 reactions 0 assignees View on GitHub
needs-triage
Dominant language
TypeScript
Stars
155
Forks
37
Avg merge
1d 17h
Merged PRs (30d)
82

Description

## Summary

A countermeasure changed to **Gap**, **Waived**, or **Decommissioned** can retain its previously entered effectiveness and continue reducing residual risk. The displayed lifecycle state says the control is unavailable/not being relied on, while scoring still grants it mitigation credit.

## Steps to reproduce

1. Create a risk with inherent score 100 and link a threat.
2. Add a countermeasure to that threat and set effectiveness to 90%.
3. Change its status to Gap, Waived, or Decommissioned.
4. Recalculate the risk.

## Actual behavior

The explicit 0.9 effectiveness wins over the status fallback and the residual score becomes 10 for all three statuses.

## Expected behavior

Statuses that represent an absent, waived, or retired control should not retain mitigation credit from a stale explicit value. A lifecycle change should clear/ignore effectiveness according to the agreed scoring policy and recalculate linked risks.

## Reproduction against current `main`

Reproduced against [`4f429a8`](https://github.com/precogly/precogly/commit/4f429a8d4e7c9b0192cf5b6cd0826db702bbebf4) using real migrated PostgreSQL models. A focused regression ran the same one-threat/one-control risk for all three statuses:

```text
gap: expected 100, received 10
waived: expected 100, received 10
decommissioned: expected 100, received 10
```

The fallback table correctly assigns zero to these statuses, but `compute_residual_score()` always chooses a non-null explicit effectiveness first:

https://github.com/precogly/precogly/blob/4f429a8d4e7c9b0192cf5b6cd0826db702bbebf4/backend/apps/threats/services.py#L11-L20

https://github.com/precogly/precogly/blob/4f429a8d4e7c9b0192cf5b6cd0826db702bbebf4/backend/apps/threats/services.py#L93-L131

## Impact

Residual scores can materially understate exposure while the UI and reports declare a control to be a gap, waived, or retired. This creates contradictory and potentially unsafe risk evidence.

## Existing issue check

- #495 covers credit leaking across different linked threats.
- Closed #388 covered stale frontend cache after backend recalculation.
- Closed #207 covered implemented/in-progress status and fallback disagreement.

This occurs with one threat and one countermeasure and is distinct from all three.

Contributor guide

Open the contributing guide

Research direction

Start in backend/apps/threats/services.py, reading the fallback table and compute_residual_score() around the referenced lines. Run the focused regression with one threat and one countermeasure across Gap, Waived, and Decommissioned statuses. Done means each status produces an inherent residual score of 100 rather than retaining stale effectiveness credit, with linked risks recalculated.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, python
Domain
backend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.