-
Notifications
You must be signed in to change notification settings - Fork 2
Specs M6 Incidents Post Incident Review
incidents.models.post_incident_review.PostIncidentReview
The learning record : ISO/IEC 27001:2022 A.5.27 (learning from information security incidents), and the bridge from an incident file into clause 10.1 / 10.2 (continual improvement, nonconformity and corrective action). It holds the determined root cause with the method used to determine it, the detection gap, the controls that failed, and the outward links that make an incident actually change something : the nonconformities it raised, the corrective action plans it produced, the risks to reassess, the vulnerabilities to register, the controls to strengthen, the ISMS changes it forced and the response plan it obliges the organisation to rewrite.
It is also the gate. An Incident cannot reach closed until its review is in approved or effectiveness_verified (RG-INC-14), so this entity carries the module's audit value : without it the register would hold a tidy chronology of things that happened and no evidence that anything changed because of them.
File: incidents/models/post_incident_review.py
ScopedModel subclass : UUID PK, sequential reference (prefix PIRV, e.g. PIRV-1), scopes M2M, tags, version, created_by, django-simple-history audit trail, and the dedicated post_incident_review lifecycle. workflow_perm_namespace is overridden to incidents.review, because the default app_label.model_name would spell incidents.postincidentreview, which matches no feature in PERMISSION_REGISTRY and would make every lifecycle permission check on the entity silently evaluate against a codename nobody holds.
The module ships as one block.
PostIncidentReviewis phase 1 and declares noPHASE 2field, but the phase markers carried by other entity specs in this module are build order, not a delivery boundary. In particular the phase 0 generalisation ofcompliance.Finding, on whichraised_findingsand the whole clause 10.2 story below depend, lands in the same release as this entity.
A.5.27 says knowledge gained from incidents shall be used to strengthen controls. That sentence is trivially satisfiable on paper and almost never satisfied in practice, because the natural end of an incident is the moment service is restored : the responders are exhausted, the pressure is off, and the write-up slips.
Cairn answers that with a structural constraint rather than a reminder. The review is a governed row with its own lifecycle, created automatically when the incident enters the post-incident review phase (the recovered -> post_incident_review transition), and the incident's closure transition is refused while it is unapproved. There is no way to reach a closed incident that has not been through a review, on any surface : the gate lives in Incident.transition_to() and therefore applies identically to the web stepper, to DRF and to MCP.
The second structural constraint is the effectiveness_verified step, which exists because the platform could not previously answer clause 10.2 d) at all. See Effectiveness verification.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
UUID | PK, auto-generated | Unique identifier |
reference |
string | auto-generated PIRV-N, unique |
Business reference. The review carries no title of its own : exactly one exists per incident (RG-INC-31), so it is identified by its reference and its incident everywhere it is rendered, and __str__ returns PIRV-N - INCD-N. |
scopes |
relation | M2M -> context.Scope
|
Copied from the incident at creation and re-synced by Incident.save() whenever the incident's scopes change. See Scope tenancy. |
scheduled_date |
date | optional, indexed | When the review is planned. Feeds the calendar and the upcoming-deadlines widget. Left blank by the automatic creation : a review nobody has scheduled shows up as an incident parked in post_incident_review, which is the honest signal. |
held_at |
datetime | optional, write-once | When the review actually took place. Stamped by the scheduled -> in_progress transition (RG-INC-12). |
root_cause_method |
enum | required, default five_whys
|
RootCauseMethod. Naming the technique is what separates a determined root cause from a plausible guess, and it is the first thing an auditor asks after reading root_cause. |
root_cause |
text | optional, blank default | The determined cause of the nonconformity : not the symptom, not the remediation. Mandatory to leave in_progress (clause 10.2 b)). |
contributing_factors |
text | optional, blank default | Secondary factors that let the incident happen, or that made it worse than it needed to be |
detection_gap |
text | optional, blank default | Why it was not detected earlier. Drives monitoring improvements (A.8.16) and is what makes the mean-time-to-detect indicator actionable rather than decorative. |
containment_assessment |
text | optional, blank default | Whether the response itself was adequate and timely : the honest verdict on the A.5.26 handling, distinct from the verdict on the controls that failed |
what_went_well |
text | optional, blank default | Practices to keep |
what_failed |
text | optional, blank default | Practices to change |
recurrence_likelihood |
enum | optional, blank default |
context.constants.Criticality (low, medium, high, critical). Clause 10.2 b) 3) : whether similar nonconformities exist, or could occur. Reuses the scale severity, assets and suppliers already share; no parallel enum is created. |
similar_incidents_checked |
boolean | required, default False
|
Must be True to leave in_progress. Confirms that clause 10.2 b) 3) was actually performed rather than silently skipped. A boolean here is defensible precisely because it is gated : it cannot be left False and still produce an approved review. |
risk_reassessment_required |
boolean | required, default False
|
The incident invalidates a registered risk evaluation. Fires RISK_REVIEW_TRIGGERED_BY_INCIDENT to the risk owners. |
response_plan_update_required |
boolean | required, default False
|
The IncidentResponsePlan itself must change : A.5.27 feeding back into A.5.24 |
training_required |
boolean | required, default False
|
Awareness or training action needed (A.6.3) |
effectiveness_review_date |
date | optional, indexed | Date the corrective actions' effectiveness will be verified. Required to reach approved, which is what puts the clause 10.2 d) verification on the calendar instead of in someone's memory. |
effectiveness_reviewed_at |
datetime | optional, write-once | When effectiveness was actually verified. Stamped by the approved -> effectiveness_verified transition (RG-INC-12). |
effectiveness_verdict |
enum | optional, blank default |
EffectivenessVerdict. Required non-blank to reach effectiveness_verified. Clause 10.2 f) retained documented information on the results of the corrective action. |
effectiveness_notes |
text | optional, blank default | The evidence supporting the verdict : what was measured, tested or observed, and when |
workflow_state |
string | indexed, default draft
|
Lifecycle step (post_incident_review). Never written directly : see Lifecycle. |
tags |
relation | M2M -> context.Tag
|
|
version |
int | auto-incremented | |
created_by |
relation | FK -> User | |
created_at / updated_at
|
datetime | auto | Timestamps |
| Name | Type | Target | Reverse accessor | Description |
|---|---|---|---|---|
incident |
OneToOne, PROTECT, required |
Incident | post_incident_review |
Exactly one review per incident (RG-INC-31). PROTECT means an incident that has been reviewed can never be deleted. |
response_plan |
FK, SET_NULL, optional |
IncidentResponsePlan | post_incident_reviews |
The plan this review concludes must be updated. Copied from the incident at creation; editable, because the review may conclude that a different plan is the one at fault. |
facilitator |
FK -> User, SET_NULL, optional |
User | facilitated_post_incident_reviews |
Who ran the review. Fills assessor on every nonconformity the review raises : see Nonconformities and assessor. |
effectiveness_reviewed_by |
FK -> User, SET_NULL, optional |
User | verified_post_incident_reviews |
Who verified that the corrective action worked. Required to reach effectiveness_verified. |
participants |
M2M -> User | User | post_incident_reviews |
Who took part. Recipients of POST_INCIDENT_REVIEW_DUE. |
raised_findings |
M2M | compliance.Finding |
post_incident_reviews |
The nonconformities this review raised, with source = incident and incident set : the one ISO 27001 clause 10.2 register, reached through the phase 0 generalisation of Finding. |
corrective_action_plans |
M2M | compliance.ComplianceActionPlan |
post_incident_reviews |
Clause 10.2 c) corrective actions, reusing the existing 8-step action-plan lifecycle with its owner, assignees, target date, progress, cost estimate and cancellation rules |
failed_controls |
M2M | compliance.Requirement |
failing_post_incident_reviews |
The controls that were in place and did not hold |
controls_to_strengthen |
M2M | compliance.Requirement |
improving_post_incident_reviews |
The controls the organisation has decided to reinforce as a result |
identified_risks |
M2M | risks.Risk |
post_incident_reviews |
Risks the incident revealed or invalidated. Pickers use linkable_or_linked(). |
identified_vulnerabilities |
M2M | risks.Vulnerability |
post_incident_reviews |
Weaknesses to register in the existing vulnerability register, never a parallel table |
isms_changes |
M2M | reports.IsmsChange |
post_incident_reviews |
Clause 10.2 e) : the changes to the ISMS this incident forced. See ISMS changes. |
failed_controls and controls_to_strengthen are deliberately two distinct M2Ms on the same target rather than one list with a role column. They answer two different questions - what broke and what we are doing about it - and an auditor reads them side by side. A single list would make the review look complete while saying nothing.
Four of these M2Ms declare the reverse accessor post_incident_reviews on four different targets (compliance.Finding, compliance.ComplianceActionPlan, risks.Risk, risks.Vulnerability, plus reports.IsmsChange and settings.AUTH_USER_MODEL for participants). That is legal because the targets differ, and none of those names is taken today. It does mean finding.post_incident_reviews and user.post_incident_reviews are different relations : never write obj.post_incident_reviews in shared code without naming the model it hangs off.
ordering = ["-scheduled_date"]- Indexes on
(workflow_state,),(scheduled_date,),(effectiveness_review_date,) -
OneToOneFieldonincidentsupplies its own uniqueness; no extra constraint is declared
Declared in incidents/constants.py.
| Value | Label |
|---|---|
five_whys |
5 Whys |
ishikawa |
Ishikawa |
fault_tree |
Fault tree analysis |
timeline_analysis |
Timeline analysis |
barrier_analysis |
Barrier analysis |
other |
Other |
| Value | Label |
|---|---|
effective |
Effective |
partially_effective |
Partially effective |
not_effective |
Not effective |
Declared once, in compliance/constants.py, by phase 0, alongside FindingSource, and imported by incidents. The canonical definition is Finding; this entity adds no value of its own. The same enum types PostIncidentReview.effectiveness_verdict and compliance.Finding.effectiveness_verdict, so the propagation described below is a straight copy and the two fields can never drift apart. incidents imports compliance; compliance does not import incidents except through the string-referenced incidents.Incident FK, so there is no cycle.
recurrence_likelihood reuses context.constants.Criticality and declares no enum of its own.
A review whose only output is prose is a story. The outward links are what make it a control.
| Outcome | Where it lands | Field on the review | Clause |
|---|---|---|---|
| A nonconformity is raised |
compliance.Finding with source = incident, incident set, assessment null |
raised_findings |
10.2 (the single register) |
| A corrective action is decided | compliance.ComplianceActionPlan |
corrective_action_plans |
10.2 c) |
| A control is named as having failed | compliance.Requirement |
failed_controls |
A.5.27, 10.2 b) |
| A control is to be reinforced | compliance.Requirement |
controls_to_strengthen |
A.5.27, 10.1 |
| A risk must be reassessed or registered |
risks.Risk (RG-INC-34) |
identified_risks + risk_reassessment_required
|
6.1.2, 8.2 |
| A weakness must be registered | risks.Vulnerability |
identified_vulnerabilities |
A.8.8 |
| The ISMS itself must change | reports.IsmsChange |
isms_changes |
10.2 e) |
| The response procedure must change | IncidentResponsePlan |
response_plan + response_plan_update_required
|
A.5.24, A.5.27 |
| Awareness or training is needed | a ComplianceActionPlan against the A.6.3 requirement |
training_required + corrective_action_plans
|
A.6.3 |
Every one of these targets already exists in the platform. No entity in this list was invented for m6, and that is the point : an incident that produces a nonconformity produces the same kind of nonconformity an audit produces, lands in the same register, is scored the same way and reaches the same management review.
Phase 0 generalises compliance.Finding from audit finding to nonconformity register entry : assessment becomes nullable (CASCADE -> SET_NULL), a source enum is added (audit | incident | management_review | monitoring | complaint, default audit), an incident FK is added, and the three effectiveness fields are added.
Finding.assessor is today a required PROTECT foreign key to AUTH_USER_MODEL with no null=True. A nonconformity raised by a post-incident review has no auditor, so phase 0:
- makes it
null=True, blank=True, keeping the column and every existing row untouched; - re-labels it
verbose_name = _("Raised by"), a label change only : no column rename, no data migration, no reverse accessor change (user.findingsis unchanged); - adds a
clean()requiring it wheneversource == audit, so nothing about the audit path is loosened.
This review fills it. Gate GP-02 (in_progress -> submitted) normalises every row in raised_findings, inside the transition's atomic block and idempotently :
for finding in review.raised_findings.all():
finding.source = FindingSource.INCIDENT
finding.incident = review.incident
if finding.assessor_id is None:
finding.assessor = review.facilitator or user
finding.save()The same normalisation runs again on entry to approved, so a nonconformity attached during a send-back-and-rework loop is normalised too. assessment is left null : fabricating an audit to hang an incident's nonconformity off is exactly the practice the generalisation exists to end.
Two consequences worth stating rather than discovering:
-
Incident-born findings reach management review section 4a for free.
reports/management_review.pybuilds section 4a withFinding.objects.select_related("assessment", "assessor"), filtered oncreated_atandfinding_typeonly : it is source-agnostic by construction, andselect_relatedon a now-nullable FK is null-safe. Nothing in that module changes. -
Assessment-scoped queries stay audit-only by construction, because they walk the reverse accessor from a
ComplianceAssessmentand an incident-born finding has none. The two statements are not in tension : section 4a is deliberately source-agnostic, the assessment-scoped scoring paths are deliberately assessment-scoped, and the module ships a test for each.
Phase 0 also introduces the compliance.finding permission feature and re-gates the existing MCP tools (list_findings, get_finding, create_finding, update_finding, delete_finding) and the existing finding views from compliance.assessment.* to compliance.finding.*. That is a breaking contract change for any live MCP integration and is recorded as such : without it, an incident responder would need audit permissions to write down a nonconformity.
RG-INC-35 : corrective work is recorded exclusively as compliance.ComplianceActionPlan rows linked from corrective_action_plans. IncidentResponseAction exists only for in-incident operational steps and carries a plain status column, never a lifecycle. The distinction is clean : stop the bleeding is a response action, make sure this cannot happen again is a corrective action plan.
The plan's own required fields (owner, gap_description, remediation_plan, priority, target_date) belong to the compliance module and are not duplicated here. The New corrective action plan button on the review opens the compliance form modal with requirements pre-seeded from controls_to_strengthen.
One mechanical point applies to raised_findings, corrective_action_plans, identified_risks and identified_vulnerabilities alike. RG-INC-37 requires every link picker to filter through linkable_or_linked(), and a freshly created target is not yet linkable : a new Finding runs the core default lifecycle and is linkable only in validated, a new ComplianceActionPlan is linkable only from to_implement onward. The create actions therefore create the row and attach it in the same atomic block, so the link exists before the target is linkable; the manual picker then uses linkable_or_linked(), which keeps that already-attached draft row visible while offering only linkable rows for new links. This is precisely what linkable_or_linked() (core/lifecycle.py linkable_or_linked()) is for, and it needs no exception to RG-INC-37.
A risk revealed by an incident is a risks.Risk with risk_source = RiskSourceType.INCIDENT, source_entity_type = "incidents.Incident" and source_entity_id = incident.pk, reusing the existing generic back-pointer (RG-INC-34). RiskSourceType.INCIDENT already exists in risks/constants.py and is unused today; m6 is its first writer.
The review's identified_risks M2M is not a duplicate of that back-pointer. They record two different facts : the back-pointer says this risk came from that incident, the M2M says this review identified it. A risk that already existed in the register before the incident materialised has no back-pointer to set and is still an outcome of the review.
risk_reassessment_required = True additionally fires RISK_REVIEW_TRIGGERED_BY_INCIDENT. Independently of the review, RG-INC-36 makes an incident that realises a risk carrying an active risks.RiskAcceptance force that acceptance under review, through a derived query hung off the existing expire_risk_acceptances sweep.
Clause 10.2 e) is make changes to the information security management system, if necessary. Answering it with a boolean would be a checkbox where an auditor expects a record, and a real entity already exists : reports.IsmsChange, the clause 9.3.3 management-review output, with its ICHG reference, its change type, its impact analysis, its affected scopes and frameworks, its owner, its status and its target and implementation dates.
The review therefore declares isms_changes = models.ManyToManyField("reports.IsmsChange", blank=True, related_name="post_incident_reviews", verbose_name=_("ISMS changes")). This costs no schema change on the reports side : related_name creates a reverse accessor, not a column, the join table belongs to the incidents migration, and post_incident_reviews is free on that model. There is no circular dependency : incidents imports reports, and reports does not import incidents.
Four honest limits an implementer must know before writing the field:
-
The class is
IsmsChange, notISMSChange.reports/models/management_review.pydeclaresclass IsmsChange, andManagementReviewDecision.linked_isms_changereferences it as"reports.IsmsChange". Use that exact spelling in the M2M string reference. -
IsmsChange.reviewis a requiredCASCADEFK toManagementReview. An ISMS change cannot exist outside a management review. The post-incident review therefore links an existing change; it cannot create one from its own page. The real workflow is the correct one : the review concludes the ISMS must change, the change is tabled at the next management review as a clause 9.3.3 output, and it is linked back from here. The picker's empty state says so and links to the management review module rather than offering a create button that cannot work. -
IsmsChangeis a plainmodels.Model, not aBaseModel: noscopes, noworkflow_state, no lifecycle, noHistoricalRecords-backed lifecycle events, and a plainstatuscolumn (proposed,approved,in_progress,implemented,rejected).linkable_or_linked()returns the queryset unchanged for a model with no lifecycle field, so the picker filters onstatusinstead and excludesrejected. This is the single place in the module where RG-INC-37's governance-helper rule does not apply, because the target has no governance to consult. -
The
CASCADEonIsmsChange.reviewis a real exposure. Deleting a management review destroys its ISMS changes, and the incident's clause 10.2 e) evidence silently becomes an empty list rather than an error, because an M2M link disappears with its target. Hardening that FK belongs to the management-review module, not to m6; m6 states the limit and the review detail page renders "no ISMS change recorded" rather than implying none was ever needed.
This is the reason the lifecycle has a step after approved.
The action-plan lifecycle proves that an action was implemented. Its steps run new -> to_define -> to_validate -> to_implement -> implementation_to_validate -> validated -> closed (compliance/lifecycles.py _action_plan_steps()), and the validation in implementation_to_validate -> validated is validation of the implementation : somebody confirms the thing was done. Nothing in that machine, or anywhere else in the platform before m6, records whether it worked.
Clause 10.2 d) requires the organisation to review the effectiveness of any corrective action taken, and 10.2 f) requires documented information on the results. A closed action plan answers neither. That is a platform-wide gap, not an incident-module gap, which is why phase 0 mirrors the same three fields onto compliance.Finding : an audit nonconformity gets the identical record.
The mechanism:
-
submitted -> approvedrequireseffectiveness_review_date(gate GP-03). Approval is not allowed to be the end of the story, and the date lands on the calendar feed and inbuild_upcoming_deadlinesthe same day. - When that date falls due,
POST_INCIDENT_REVIEW_DUEnotifies the facilitator and the participants. -
approved -> effectiveness_verifiedrequiresincidents.review.approve, a mandatory comment, a non-blankeffectiveness_verdictand a non-nulleffectiveness_reviewed_by(gate GP-04, RG-INC-32). It stampseffectiveness_reviewed_atand propagates the verdict onto every linkedFinding, copyingeffectiveness_verdict,effectiveness_reviewed_atandeffectiveness_reviewed_by. - The propagation is a snapshot at that instant, not a live mirror. The review's verdict is the aggregate judgement; a nonconformity whose individual verdict differs is edited on the finding itself afterwards, and the finding's own history records the divergence. Nothing re-writes a finding after this transition.
The propagation respects RG-FND-06 : a finding with no linked ComplianceActionPlan in a reportable state and no recorded justification for having none is skipped rather than stamped, and the transition reports which findings it skipped. A verdict about the effectiveness of nothing is not a record, and the review is not allowed to manufacture one in bulk.
not_effective and partially_effective are first-class outcomes, not failure states. A verified-ineffective corrective action is a better record than a closed action plan with no verdict at all : it is the input that produces the next nonconformity. The review does not reopen itself on a not_effective verdict, and the module deliberately does not automate that : deciding what to do about a corrective action that did not work is a management judgement, and the honest artefacts of it are a new Finding and a new ComplianceActionPlan, both reachable from the same page.
Self-verification is permitted and visible : facilitator and effectiveness_reviewed_by are rendered side by side in the sidebar, so a review verified by the person who ran it reads as exactly that.
LIFECYCLE_NAME = "post_incident_review", layout="graph", generated by lifecycle_from_state_flags() in incidents/lifecycles.py from the state and transition constants in incidents/constants.py, and registered from IncidentsConfig.ready().
Unlike incident and incident_evidence, this lifecycle needs no step trigger, so it keeps the generated form the project's rules prescribe. It declares both bookend steps explicitly : see The archive and restore bookends.
lifecycle_name_for()(core/lifecycle.pylifecycle_name_for()) resolvesLIFECYCLE_NAMEonlyif name and name in LIFECYCLE_REGISTRY. Anincidents/apps.pywhoseready()forgets to importincidents.lifecyclestherefore fails silently : this model would quietly run the default 4-state lifecycle, with noeffectiveness_verifiedstep and no gates, in tests as well as in production, and RG-INC-14 would then compare the incident's review against steps that do not exist. The module ships a test assertingPostIncidentReview.get_lifecycle().name == "post_incident_review".
Eight steps : one draft entry, four review stages, two domain terminal exits and the generic archived exit.
| Code | Label | StepKind | In reports | Linkable | Deletable | Tone | Meaning |
|---|---|---|---|---|---|---|---|
draft |
Draft | DRAFT |
no | no | yes | neutral |
A review row that exists but has not been opened. The landing step of every insert (see Creation and the initial step). |
scheduled |
Scheduled | INTERMEDIATE |
yes | no | yes | secondary |
The learning phase is open. Created automatically on the incident's recovered -> post_incident_review transition; scheduled_date may still be blank. |
in_progress |
In progress | INTERMEDIATE |
yes | no | no | info |
Being held and written up. held_at is stamped on entry. |
submitted |
Submitted | INTERMEDIATE |
yes | yes | no | primary |
Root cause determined and recurrence checked, awaiting approval |
approved |
Approved | INTERMEDIATE |
yes | yes | no | success |
The minimum state an incident needs to close (RG-INC-14). The effectiveness review is scheduled. |
effectiveness_verified |
Effectiveness verified |
ARCHIVED (terminal) |
yes | yes | no | dark |
The clause 10.2 d)/f) record : the corrective actions were verified and a verdict is on file |
cancelled |
Cancelled |
ARCHIVED (terminal) |
no | no | no | muted |
The review of an incident that turned out not to be one. See gate GP-06. |
archived |
Archived | ARCHIVED |
no | no | no | muted |
The generic exit, declared explicitly |
effectiveness_verified keeps counts_in_reports = True and stays linkable : a verified review is exactly what the annual register, the management review and the A.5.27 evidence pack are about, and a terminal step here means finished, not withdrawn. cancelled does not count, because the review never concluded anything.
scheduled and submitted are the two steps where the governance flags carry real weight. scheduled is deletable = True only so a review created by hand in error can be removed without an approver; delete() is overridden to refuse the automatic case (see Deleting a review). submitted is the first linkable step, so a review cannot be quoted from a report or attached anywhere until its root cause has actually been determined.
permission_action is the suffix appended to workflow_perm_namespace (incidents.review), so update means incidents.review.update and approve means incidents.review.approve.
| Verb | Transition | permission_action |
requires_comment |
Side effects |
|---|---|---|---|---|
| Schedule | draft -> scheduled |
update |
no | Hand-declared. The step every auto-created review is moved to immediately after insert. |
| Hold the review | scheduled -> in_progress |
update |
no | Stamps held_at
|
| Submit | in_progress -> submitted |
update |
no |
Gate GP-02. Normalises every row in raised_findings (source, incident, assessor). |
| Send back for rework | submitted -> in_progress |
update |
yes | The comment is the approver's reason. held_at is not re-stamped (write-once). |
| Approve | submitted -> approved |
approve |
yes |
Gate GP-03. Requires effectiveness_review_date; re-runs the finding normalisation. |
| Verify effectiveness | approved -> effectiveness_verified |
approve |
yes |
Gate GP-04. Stamps effectiveness_reviewed_at; propagates the verdict onto each linked Finding. |
| Cancel | * -> cancelled |
approve |
yes | Gate GP-06. |
| Archive | * -> archived |
approve |
yes | Hand-declared, not auto-wired |
| Restore | archived -> draft |
approve |
no | Hand-declared, and refused by gate GP-05 |
There is no lifecycle_transition_url_name override : every transition posts to the generic workflow:transition endpoint, and every gate below lives on the model.
Two engine behaviours are worth naming, because both are easy to assume away:
-
transitions_from()(core/lifecycle.pyLifecycle.transitions_from()) matches a transition whose source isANYfrom every step except the target itself, and does not exclude terminal steps. Left ungated,Cancelwould be offered fromeffectiveness_verifiedand fromarchived, andArchivefromcancelled. Gate GP-06 closes the first; the archive edge from a terminal step is harmless and is left alone. -
BaseModel.transition_to()firesnotify_lifecycle_submitted(RG-LC-06) only whenlifecycle.name == DEFAULT_LIFECYCLE_NAME and target == "pending"(context/models/base.pyBaseModel.transition_to()). A bespoke lifecycle'ssubmittedstep therefore fires nothing, whatever it is called. The approval request on this entity is an explicit module notification, not an inherited one.
lifecycle_from_state_flags() auto-wires draft -> <initial step>, ANY -> archived and archived -> draft only when the corresponding step is absent from the state-flag list (core/lifecycle.py lifecycle_from_state_flags()). The auto-wired archive and restore edges carry no permission_action and no requires_comment, and user_can_perform() (core/lifecycle.py user_can_perform()) allows any transition whose permission_action is empty.
Because draft and scheduled are both deletable = True, leaving those edges generated would give anyone able to reach the transition endpoint an archive -> restore -> delete path out of an approved review. On this entity that path destroys the A.5.27 record an incident was closed on, and with it the only evidence that closure was ever legitimate : the incident stays closed, and the gate that let it close no longer exists.
The lifecycle therefore:
- declares
draftandarchivedexplicitly among its state-flag items, sohas_draftandhas_archivedare bothTrueand nothing is auto-wired; - hand-declares
ANY -> archivedwithpermission_action="approve"andrequires_comment=True; - hand-declares
archived -> draftwithpermission_action="approve", additionally gated by GP-05; - hand-declares the
draft -> scheduledentry edge, which is no longer auto-wired either.
All four edges are listed in POST_INCIDENT_REVIEW_TRANSITIONS in incidents/constants.py with explicit actions, so the state literals stay in one file (RG-INC-37). The same correction is applied to every lifecycle in the module. IncidentResponsePlan needs none of it : it runs the core default lifecycle, whose archive edge already carries permission_action="approve" and which has no restore transition at all.
The module ships a regression test that walks a review from approved to archived, attempts archived -> draft as a holder of incidents.review.update, and asserts both that the transition is refused and that the row still exists.
Per RG-INC-08, every gate below lives in a transition_to() override on PostIncidentReview, never in Transition.form_class, allowed_roles or allowed_users. lifecycle_to_json() (core/lifecycle.py lifecycle_to_json()) omits those three by design, lifecycle_from_json() (core/lifecycle.py lifecycle_from_json()) rebuilds transitions without them, and get_lifecycle() (core/lifecycle.py get_lifecycle()) prefers the post_migrate-seeded LifecycleDefinition row over the code default, so a gate declared that way is green in an in-memory unit test and absent on every migrated database. All three write surfaces funnel through BaseModel.transition_to() (core/workflow_views.py WorkflowTransitionView.post(), accounts/api/mixins.py _lifecycle_transition(), mcp/tools.py _transition_handler()), so the model override is the one place that binds web, API and MCP at once.
Each gate raises a translated ValidationError naming the missing precondition, before perform_transition() runs, and the whole transition body runs inside transaction.atomic(). Gate identifiers are local to this entity.
| Gate | Transition | Refused unless |
|---|---|---|
| GP-01 Opening | scheduled -> in_progress |
The parent incident is in post_incident_review or later. Holding a review for an incident still being contained is not a review, it is a status meeting. Stamps held_at. |
| GP-02 Submission (RG-INC-32) | in_progress -> submitted |
root_cause is non-blank and similar_incidents_checked is True. Clause 10.2 b) and b) 3). The transition then normalises raised_findings : source = incident, incident = review.incident, and assessor = facilitator or user where blank. |
| GP-03 Approval (RG-INC-32) | submitted -> approved |
Holder of incidents.review.approve, a mandatory comment, and a non-null effectiveness_review_date. Approving a review with no verification date scheduled is how clause 10.2 d) is missed, so the gate refuses it rather than trusting a reminder. |
| GP-04 Effectiveness verdict (RG-INC-32) | approved -> effectiveness_verified |
Holder of incidents.review.approve, a mandatory comment, a non-blank effectiveness_verdict and a non-null effectiveness_reviewed_by. Stamps effectiveness_reviewed_at and propagates the verdict onto each linked Finding. |
| GP-05 Restore | archived -> draft |
No core.LifecycleEvent on this review records a step other than draft or archived. A review that ever reached scheduled can be archived but never restored into a deletable step. Mirrors Incident G-07. |
| GP-06 Cancellation | * -> cancelled |
The parent incident is itself in a terminal step (reclassified or archived), and the current step is not already terminal. A live incident must have a live review to reach closure, so there is deliberately no path that leaves a closeable incident holding a cancelled one; and because the OneToOne is PROTECT and RG-INC-31 allows exactly one review per incident, a cancelled review can never be replaced. This is also why no cancelled -> scheduled edge exists : a reclassified incident's review is not resumed, and a reclassified incident cannot be un-reclassified. |
| GP-07 Write-once stamps (RG-INC-12) | all |
held_at and effectiveness_reviewed_at are stamped by the override only : excluded from every ModelForm, read_only in every serializer, absent from every MCP writable_fields list, and never cleared. Write-once is prevented at application level and detected through HistoricalRecords; QuerySet.update(), bulk_update() and raw SQL bypass save(). |
The closure gate itself lives on the other side of the relation : Incident G-05 refuses post_incident_review -> closed unless this review exists and is in approved or effectiveness_verified. Membership is tested against the step codes exported from incidents/constants.py, never against literals (RG-INC-37).
BaseModel.save() calls _ensure_initial_step() (context/models/base.py BaseModel._ensure_initial_step()) on every insert, and Lifecycle.initial_step (core/lifecycle.py Lifecycle.initial_step) returns the single StepKind.DRAFT step. The workflow_state field default is the literal "draft", which is a valid step of this lifecycle, so _ensure_initial_step() leaves it alone and the row lands in draft.
A review is never "created in" scheduled. Writing PostIncidentReview.objects.create(..., workflow_state="scheduled") would stick - the snap only fires on a blank or unknown value - but it would leave no core.LifecycleEvent row, so the review would exist with no record of ever having been opened. A review left in draft because nobody made the second call is worse still : draft is deletable = True, the review is missing from every "reviews to hold" list, and it silently blocks incident closure through RG-INC-14 with no explanation on any screen.
The incident's recovered -> post_incident_review transition therefore does, inside one transaction.atomic() block:
review = PostIncidentReview(incident=incident, response_plan=incident.response_plan)
review.save()
review.scopes.set(incident.scopes.all())
review.transition_to("scheduled", user, enforce_permission=False)enforce_permission=False is correct here : the permission was already checked on the parent transition the user actually performed, and the review is a consequence of it, not a separate act by the user. The creation is idempotent : the transition creates the review only when incident.post_incident_review does not already exist, so re-entering post_incident_review after a reopen never produces a second row and never resets the first.
facilitator, scheduled_date and every narrative field are left blank. Nothing is guessed : a review with no facilitator and no date is visibly unstarted, which is the true state of affairs the moment service is restored.
The module ships regression tests asserting that PostIncidentReview.objects.create(...).workflow_state == "scheduled" fails, that after incident.transition_to("post_incident_review", user) the review is in scheduled with exactly one matching core.LifecycleEvent, and that a second entry into that step leaves the review count and the LifecycleEvent count unchanged.
delete() is overridden to refuse any review whose parent incident has not reached a terminal step, mirroring the IncidentNotification override that refuses to delete a generated obligation. The reason is the same in both cases : the row is the answer to a question the platform asked on the organisation's behalf, and deleting it destroys the evidence that the question was ever considered. Concretely, deleting an auto-created review would strand its incident, which can then never close (RG-INC-14) and can never obtain a second review (RG-INC-31, OneToOne).
From in_progress onward no step is deletable on any surface, so BaseModel.delete() raises LifecycleProtectedError before the override is even consulted. And PROTECT on incident means the incident can never be deleted while its review exists, which from detected onward it already could not be (RG-INC-07).
RG-INC-38. PostIncidentReview is a ScopedModel and carries its own scopes, so ScopeFilterMixin (accounts/mixins.py ScopeFilterMixin.get_queryset()) and ScopeFilterAPIMixin (accounts/api/mixins.py get_queryset()) filter its list view and viewset with no extra work, and mcp/tools.py _filter_by_scopes() handles the direct scopes M2M it already supports. Unlike IncidentEvidence and IncidentNotification, this entity needs none of the three scope_parent_lookup call-site extensions phase 1 makes, and its workflow:transition and history endpoints are already guarded correctly today.
That is exactly why the scopes must not be allowed to drift. They are copied from the incident at creation and re-synced by Incident.save() whenever the incident's scopes change (RG-INC-31). Without the re-sync, re-scoping an incident would leave its review visible to the old tenant and invisible to the new one, and the review is where the root cause is written down.
The seed and the tests both assert set(review.scopes.all()) == set(review.incident.scopes.all()) after a re-scope.
| ID | Rule |
|---|---|
| RG-INC-08 | Every audit gate is enforced in a transition_to() override on the model, never through Transition.form_class, allowed_roles or allowed_users, because lifecycle_to_json drops those and the seeded LifecycleDefinition row wins at runtime. |
| RG-INC-12 |
held_at and effectiveness_reviewed_at are stamped by the transition_to() override only. They are excluded from every ModelForm, are read_only in every serializer, and are absent from every MCP writable_fields list. Prevention at application level, detection via HistoricalRecords. |
| RG-INC-14 | An incident cannot reach closed while its post-incident review is in any step other than approved or effectiveness_verified. The gate lives in Incident.transition_to() and applies identically to the web stepper, DRF and MCP. |
| RG-INC-17 | An exercise (is_exercise = True) still requires a review to close : the review is the A.5.24 exercise report, and its what_failed and response_plan_update_required fields are the plan-testing evidence. It runs the identical lifecycle with identical gates, and is excluded from every KPI, indicator, report, calendar deadline and dashboard count. |
| RG-INC-31 | Exactly one PostIncidentReview per incident (OneToOne, PROTECT), created automatically on entry to post_incident_review : saved, then transitioned to scheduled with enforce_permission=False in the same transaction. Its scopes are copied from the incident at creation and re-synced by Incident.save() whenever the incident's scopes change, so the review can never drift out of scope alignment. |
| RG-INC-32 | Leaving in_progress requires a non-blank root_cause and similar_incidents_checked = True; reaching approved additionally requires an effectiveness_review_date; reaching effectiveness_verified requires incidents.review.approve, a mandatory comment, a non-blank effectiveness_verdict and a non-null effectiveness_reviewed_by. This is the ISO 27001 clause 10.2 d)/f) record the platform does not hold today. |
| RG-INC-34 | A nonconformity raised by a review is a compliance.Finding with source = incident and incident set, assessment left null : the one ISO clause 10.2 register. A risk revealed by an incident is a risks.Risk with risk_source = RiskSourceType.INCIDENT and the existing generic back-pointer. No second nonconformity model, no new FK on Risk. |
| RG-INC-35 | Corrective work is recorded exclusively as compliance.ComplianceActionPlan rows linked from corrective_action_plans. IncidentResponseAction exists only for in-incident operational steps and carries a plain status column, never a lifecycle. |
| RG-INC-36 | An incident realising a risk that carries an active risks.RiskAcceptance forces that acceptance under review, through a derived query hung off the existing expire_risk_acceptances sweep. risk_reassessment_required on the review is the manual counterpart, and both fire RISK_REVIEW_TRIGGERED_BY_INCIDENT. |
| RG-INC-37 | Every report, KPI, indicator, calendar feed and link picker filters through reportable() / linkable() / linkable_or_linked() / deletable_states(). No review state literal appears anywhere outside incidents/constants.py. The single documented exception is the isms_changes picker, whose target has no lifecycle at all. |
| RG-INC-38 | Scope tenancy : PostIncidentReview carries its own scopes (ScopedModel) and needs none of the scope_parent_lookup extensions the module's non-scoped children require. |
| RG-INC-39 | The module has exactly six permission features and never grows. incidents.review gates this entity and nothing else. |
Base path /api/v1/incidents/, router registration post-incident-reviews.
-
GET /api/v1/incidents/post-incident-reviews/: list, filtered byPostIncidentReviewFilter(incident_id,status,facilitator_id,root_cause_method,recurrence_likelihood,effectiveness_verdict,risk_reassessment_required,response_plan_update_required,training_required,scheduled_after/scheduled_before,effectiveness_review_before,scope_id) -
POST /api/v1/incidents/post-incident-reviews/andPOST /api/v1/incidents/post-incident-reviews/batch/(max 100 items, non-atomic, per-item{index, status, id, reference}) -
GET/PUT/PATCH/DELETE /api/v1/incidents/post-incident-reviews/<uuid>/:DELETEsucceeds only under the conditions in Deleting a review; otherwise the endpoint returns 409 -
GET/POST /api/v1/incidents/post-incident-reviews/<uuid>/transition/: supplied byLifecycleAPIMixin, routed throughtransition_to(enforce_permission=True), so every gate above applies identically to an API caller -
GET /api/v1/incidents/post-incident-reviews/<uuid>/history/:core.history.build_timeline, mergingLifecycleEventandHistoricalRecords
Viewset stack in the house order : BatchCreateMixin, ScopeFilterAPIMixin, LifecycleAPIMixin, HistoryAPIMixin, CreatedByMixin, viewsets.ModelViewSet. Permissions follow the newest module precedent (trust_center/api/views.py _ManagedViewSet) : ModulePermission directly, plus the module's _IncidentViewSet base fixing permission_module = "incidents" and custom_action_map = {"transition": "update"}, with permission_feature = "review" on this viewset. Another app's ModulePermission subclass (ContextPermission) is not imported.
Two serializers : PostIncidentReviewSerializer (full) and PostIncidentReviewListSerializer for the index, switched on self.action == "list". read_only_fields cover id, reference, created_by, created_at, updated_at, version, held_at and effectiveness_reviewed_at. status is exposed as CharField(source="workflow_state", read_only=True), and incident_reference, facilitator_name and effectiveness_reviewed_by_name as read-only display fields backed by model properties. incident is writable on create and read_only on update : re-pointing an existing review at a different incident would silently strand the first one.
Phase 0 additionally exposes /api/v1/compliance/findings/ as a standalone router registration : findings have no standalone route today, every finding URL being nested under an assessment, which an incident-born nonconformity has none of.
-
_register_crud(server, "post_incident_review", PostIncidentReview, "incidents.review", ...)generateslist_post_incident_reviews,get_post_incident_review,create_post_incident_review,batch_create_post_incident_reviews,update_post_incident_review,delete_post_incident_review,transition_post_incident_review,post_incident_review_allowed_transitions,get_post_incident_review_history. - Filters :
incident_id,status,facilitator_id,root_cause_method,effectiveness_verdict. Search fields :reference,root_cause,detection_gap,what_failed. -
m2m_fieldsmapsscope_ids,tag_ids,participant_ids,raised_finding_ids,corrective_action_plan_ids,failed_control_ids,control_to_strengthen_ids,identified_risk_ids,identified_vulnerability_idsandisms_change_ids. - The entity is a
ScopedModel, so_register_crudis called with the defaultscope_filtered=Trueand noscope_parent_lookupargument :_filter_by_scopes()already handles a directscopesM2M. -
root_cause_method,recurrence_likelihoodandeffectiveness_verdicteach carry afield_overridesentry with an explicitenumlist; every FK id argument names its lookup tool in its description (Use list_incidents to get valid IDs,Use list_findings to get valid IDs,Use list_isms_changes to get valid IDs);held_atandeffectiveness_reviewed_atnever appear inwritable_fields. - Attaching a nonconformity through
raised_finding_idsrequiresincidents.review.update; creating one requirescompliance.finding.create, the phase 0 re-gating of the existingcreate_findingtool away fromcompliance.assessment.create. An agent that holds only incident permissions can link an existing finding and cannot invent one.
mcp/tools.py HELP_TEXT gains PostIncidentReview=PIRV in the reference-prefix block, and the entity gets its own section in TOPIC_INCIDENTS listing writable fields, enum values, filters and the reference prefix.
| Codename | Description |
|---|---|
incidents.review.read |
List and read post-incident reviews |
incidents.review.create |
Create a review by hand (the normal path creates it automatically) |
incidents.review.update |
Edit the narrative and the outward links, schedule, hold and submit the review, send it back for rework |
incidents.review.approve |
Approve a review, record the effectiveness verdict, cancel, archive, restore |
incidents.review.delete |
Delete a review under the conditions above |
incidents.review is one of the module's exactly six permission features (incident, security_event, evidence, notification, review, response_plan), each with the five standard PermissionAction verbs and no custom ones, so the six SYSTEM_GROUPS suffix lambdas grant them unchanged and the group matrix screen, which renders a hardcoded action list, displays every one of them (RG-INC-39). The rows are created and attached to the six system groups by accounts/migrations/0056_add_incidents_permissions.py.
workflow_perm_namespace = "incidents.review" is mandatory on this model : without it the namespace resolves to incidents.postincidentreview, which matches no registry feature, and every lifecycle permission check on the entity silently evaluates against a codename nobody holds.
Phase 0's separate compliance.finding feature gets its own accounts data migration in the same release. Note that granting incidents.review.update does not grant the ability to create a nonconformity : that needs compliance.finding.create, deliberately, because a nonconformity is a compliance record wherever it comes from.
List (/incidents/post-incident-reviews/) : the house stack (LoginRequiredMixin, PermissionRequiredMixin, ListSummaryMixin, PredefinedFilterMixin, AdvancedFilterMixin, SavedFilterMixin, ColumnPreferenceMixin, ScopeFilterMixin, SortableListMixin, ListView, with ListSummaryMixin strictly left of ScopeFilterMixin), page_header with accent="incidents", an #item-table-body HTMX partial and the filter offcanvas. Columns : reference, incident (reference and title), state, facilitator, scheduled_date, held_at, root cause method, effectiveness_review_date and the effectiveness verdict. A review whose effectiveness_review_date has passed while it is still in approved is flagged in the row : that is an open clause 10.2 d) obligation, and it is the single most useful thing this list shows.
Detail (/incidents/post-incident-reviews/<uuid>/) : a strict 2-column card layout, no nav-tabs.
- Left column, stacked cards:
-
Root cause :
root_cause_methodrendered as a chip besideroot_cause, thencontributing_factors,detection_gapandcontainment_assessment. The method chip sits before the cause on purpose. -
What went well / what failed : two columns on desktop, stacked on mobile, with
recurrence_likelihoodand thesimilar_incidents_checkedstate rendered as a labelled pair rather than a bare checkbox. -
Outcomes : the outward links, each as its own labelled block with an inline picker and a create and attach action : raised findings (with their type and state), corrective action plans (with owner, target date and progress), identified risks, identified vulnerabilities, failed controls, controls to strengthen, ISMS changes, and the three flags (
risk_reassessment_required,response_plan_update_required,training_required) rendered as consequences with a link to the thing each one points at. The ISMS changes block shows a "no ISMS change recorded" empty state linking to the management review module, never a create button. -
Effectiveness :
effectiveness_review_date,effectiveness_verdict,effectiveness_reviewed_by,effectiveness_reviewed_atandeffectiveness_notes. Before verification the card renders the scheduled date with a relative hint and, once past, a warning state.
-
Root cause :
- Right column, sticky sidebar :
{% workflow_badge %}, the parent incident link with its own state badge, facilitator andeffectiveness_reviewed_byside by side so self-verification is visible, participants as avatars,scheduled_dateandheld_at, the response plan link, scopes, tags, the history trigger and the lifecycle stepper.
Stepper : {% include "includes/lifecycle_stepper.html" %} fed by LifecycleStepperMixin. Never a status select, never plain buttons. This lifecycle has three StepKind.ARCHIVED steps (effectiveness_verified, cancelled, archived), so the dagre renderer draws three detached exits and needs an explicit visual check at desktop and mobile widths in both light and dark mode before merge. The transitions carrying a mandatory comment (approve, verify, send back, cancel, archive) all use the stepper's shared comment modal; no bespoke form is added.
On the incident detail page, the review appears as a summary card in the left column : state badge, facilitator, scheduled_date, a one-line root-cause extract, the counts of raised findings and corrective action plans, and a link through. When the incident is in post_incident_review and the review is not yet approved, the card states plainly that closure is blocked and why, so the operator never has to deduce the gate from a greyed-out stepper pill.
Create and update use HtmxFormMixin drawer modals, with mobile-first care on the eight multi-select widgets in the Outcomes card and on the sticky action bar : this is, after the response plan, the second-tallest form in the module, and on small screens the outcome pickers render as an accordion so the action bar stays reachable.
scheduled_date and effectiveness_review_date are both fed into the calendar under the incident category and into build_upcoming_deadlines. Reviews belonging to an exercise incident are excluded from both feeds (RG-INC-17).
| Type | Fired when | Recipients | Channel |
|---|---|---|---|
POST_INCIDENT_REVIEW_DUE |
scheduled_date or effectiveness_review_date falls due, swept daily by escalate_incident_deadlines
|
The facilitator and the participants | In-app |
RISK_REVIEW_TRIGGERED_BY_INCIDENT |
The review sets risk_reassessment_required, or an incident realises a risk carrying an active RiskAcceptance (RG-INC-36) |
The risk owners and the acceptance owner | In-app, email |
Both follow notify_lifecycle_submitted : the in-app rows are created in the same transaction as the change, rendered per recipient under translation.override(recipient.language), with email and WebSocket delivery scheduled on transaction.on_commit() so a rolled-back transition sends nothing. No new notification model is needed : accounts.Notification already targets any object through a GenericForeignKey, and naming the route incidents:post-incident-review-detail makes _target_url resolve with no special casing.
There is deliberately no review submitted for approval notification of the RG-LC-06 kind, because that helper fires only for the core default lifecycle's pending step. If one is wanted it is an explicit module notification with its own NotificationType value, not something inherited by naming a step submitted.
Every user-facing string is wrapped with _() / pgettext_lazy() in Python or {% trans %} in templates and has a French translation in locale/fr/LC_MESSAGES/django.po. A duplicate (msgctxt, msgid) pair makes manage.py compilemessages fail, and .github/workflows/tests.yml runs compilemessages before pytest, so a collision breaks CI outright.
Enum labels, field verbose names and template strings whose English already exists in the catalogue use pgettext_lazy("incident", ...) in Python and {% trans "..." context "incident" %} in templates, with a matching msgctxt "incident" block in the .po. For this entity that is a single label:
| Label | Where | Existing bare entry | Decision |
|---|---|---|---|
Other |
RootCauseMethod.OTHER |
present four times, bare entry msgstr "Autre"
|
pgettext_lazy("incident", "Other"). This is the same (msgctxt, msgid) pair DetectionSource.OTHER already declares in Incident : gettext merges the two occurrences into one entry with several #: references, which is not a duplicate. Declare the .po entry once. |
Facilitator (msgstr "Animateur"), Participants, Notes, Findings and ISMS changes already exist with a French translation that is correct in this context, so the same msgid is reused and no new entry is added. Every other field verbose name is a new bare msgid : "Scheduled date", "Held at", "Root cause method", "Root cause", "Contributing factors", "Detection gap", "Containment assessment", "What went well", "What failed", "Recurrence likelihood", "Similar incidents checked", "Risk reassessment required", "Response plan update required", "Training required", "Effectiveness review date", "Effectiveness reviewed at", "Effectiveness reviewed by", "Effectiveness verdict", "Effectiveness notes", "Raised findings", "Corrective action plans", "Failed controls", "Controls to strengthen", "Identified risks", "Identified vulnerabilities". The three EffectivenessVerdict labels ("Effective", "Partially effective", "Not effective"), declared by phase 0 in compliance, and the five non-other RootCauseMethod labels ("5 Whys", "Ishikawa", "Fault tree analysis", "Timeline analysis", "Barrier analysis") are likewise new. recurrence_likelihood renders context.constants.Criticality, whose labels are already in the catalogue and are not re-declared here.
Step and transition labels are different, and must never use pgettext_lazy. lifecycle_to_json() stringifies each label with str(...) and lifecycle_from_json() re-wraps the stored string with bare gettext_lazy (core/lifecycle.py lifecycle_from_json()), so a label carrying a msgctxt in code loses that context after the post_migrate round-trip through LifecycleDefinition and resolves to whatever the bare msgid maps to. A collision-free English label is the only correct fix; a context is not.
This lifecycle is fortunate, and the reason is worth recording so nobody "improves" it later : the French for post-incident review is revue post-incident, which is feminine, and every colliding bare entry is already in the feminine.
| Label | Existing bare entry | Decision |
|---|---|---|
draft step "Draft" |
Draft -> "Brouillon" |
Reuse. The core draft_step() emits this exact string and the French is correct. |
in_progress step "In progress" |
In progress -> "En cours" (two entries : one bare, one with msgctxt "isms change status") |
Reuse the bare entry. "En cours" is invariable in gender and correct here. |
approved step "Approved" |
Approved -> "Approuvée" (feminine; a second entry carries msgctxt "isms change status") |
Reuse the bare entry. Feminine is correct for revue. Contrast Incident, whose closed step had to be renamed because the bare "Closed" is "Clôturée". |
cancelled step "Cancelled" |
Cancelled -> "Annulée" (five entries; the bare one plus assessment, action_plan, management review status, decision status) |
Reuse the bare entry. Feminine is correct. |
archived step "Archived" |
Archived -> "Archivé" |
Reuse. The core archived_step() emits this exact string. |
| Submit transition "Submit" |
Submit -> "Soumettre" |
Reuse. A verb in the infinitive, which is what a transition label must be. |
| Approve transition "Approve" |
Approve -> "Approuver" |
Reuse. |
| Cancel transition "Cancel" |
Cancel -> "Annuler" (bare, plus one with msgctxt "permission") |
Reuse the bare entry. |
| "Archive" / "Restore" transitions | present as the core bookend labels | Reuse. |
The remaining labels are new bare msgids with no collision : the scheduled step "Scheduled" -> "Planifiée" (distinct from the existing "Planned", which is a different msgid), the submitted step "Submitted" -> "Soumise", the effectiveness_verified step "Effectiveness verified" -> "Efficacité vérifiée", and the transitions "Schedule", "Hold the review", "Send back for rework" and "Verify effectiveness". "Send back for rework" is deliberately not the core default lifecycle's "Send back to draft" : this edge returns the review to in_progress, not to draft, and a label that lies about its target is worse than a long one.
After editing the .po, verify there is no duplicate msgid without a distinguishing msgctxt.
- ISO/IEC 27001:2022 A.5.27 (learning from information security incidents), and by reference A.5.24 (planning and preparation), A.5.26 (response), A.6.3 (awareness and training), A.8.8 (technical vulnerability management), A.8.16 (monitoring activities)
- ISO/IEC 27001:2022 clause 10.1 (continual improvement) and clause 10.2 a) to f) (nonconformity and corrective action), in particular b) 3), c), d), e) and f)
- ISO/IEC 27001:2022 clause 9.3.2 d) 1) and d) 2) (management review inputs : nonconformities and corrective actions, monitoring and measurement results), reached through the generalised
compliance.Findingand the module's predefined indicators, and clause 9.3.3 (management review outputs, the home ofreports.IsmsChange) - ISO/IEC 27035-1 / -2 : the learn phase, whose structure this entity mirrors
-
Incident : the parent, the
recovered -> post_incident_reviewtransition that creates this row, and the G-05 closure gate that this row governs -
IncidentResponsePlan : the A.5.24 procedure whose
lessons_learned_proceduregoverns this review, and the planresponse_plan_update_requiredsends work back to - IncidentEvidence : the A.5.28 artefacts a review reasons from, and the module-wide archive / restore correction
-
IncidentNotification : the
delete()override pattern this entity mirrors - IncidentResponseAction : in-incident operational steps, deliberately not corrective actions (RG-INC-35)
- SecurityEvent : where a weakness identified by a review is reported when it is found outside an incident
-
README.md : module business rules, permission codenames, notifications and the phase-0
compliance.Findinggeneralisation -
governance/workflow.md and governance/lifecycle.md : the lifecycle framework,
LifecycleEventand the engine internals -
Finding : the nonconformity register phase 0 generalises, and the entity spec the
source,incident,assessorand effectiveness changes must be reflected in - ComplianceActionPlan, Requirement
- Risk, RiskAcceptance, Vulnerability
-
IsmsChange and Decision : the clause 9.3.3 outputs
isms_changeslinks to
Built from docs/ at v0.36.0. Edits made here are overwritten by the next release : open a pull request against the source instead.
- Administration
- Ask Cairn
- Assets and suppliers
- Compliance
- The dashboard
- Finding your way
- Getting started
- Incidents
- How records move
- Organisational context
- Reports and management review
- Risks
- Trust Center
- Architecture
- Configuration
- Contributing
- The documentation system
- Installation
- Internationalisation
- Operations
- Release process
- Security
- Testing
- Adding an assistant provider
- Adding a dashboard widget
- Adding a domain entity
- Declaring a lifecycle
- Adding an MCP tool
- Adding a REST endpoint
- Adding a report
- Interface conventions
- Dashboard widgets
- Lifecycles
- MCP tools
- MCP tool parameters : Assets
- MCP tool parameters : Compliance
- MCP tool parameters : Governance and context
- MCP tool parameters : General
- MCP tool parameters : Incidents
- MCP tool parameters : Reports and management review
- MCP tool parameters : Risks
- MCP tool parameters : System and administration
- MCP tool parameters : Trust Center
- Management commands
- Models
- Permissions
- REST endpoints
- Environment variables
- MCP server
- REST API
- Assistant module (Ask Cairn)
- Module 0: User Management and Access Control
- Module 1: Context and Organization
- Module 2: Asset Management
- Module 3: Compliance
- Module 4: Risk Management
- Module 4 bis - EBIOS Risk Manager
- Module 5 : Trust Center
- Module 6 : Security Incident Management
- Management review : ISO 27001:2022 compliance (clause 9.3)