-
Notifications
You must be signed in to change notification settings - Fork 2
Specs M3 Compliance Finding
compliance.models.finding.Finding
The organisation's single nonconformity register. A Finding records one departure from a requirement, a control or the organisation's own procedure, whatever surfaced it : an assessment, a security incident, a management review, routine monitoring or a complaint. It is the ISO/IEC 27001:2022 clause 10.1 (nonconformity and corrective action) record, and its effectiveness fields are the clause 10.2 d) and f) record.
Historically this entity was an audit finding : it required an assessment and existed only inside one. The security incident module (m6) forced the question of where an incident-born nonconformity lives, and the answer taken was to generalise this entity rather than to create a second register. An auditor asking "show me your nonconformities" must receive one list, not two to reconcile.
The alternative considered and rejected was an IncidentNonconformity in m6. It would have left Finding untouched and cost nothing in blast radius, but:
- clause 10.2 would be answered in two places, and the two would drift;
- the worst-finding-wins compliance scoring in
ComplianceAssessment.apply_findings_to_results()and the management-review nonconformity section would both ignore incident-born nonconformities unless every surface were rewired twice; - an organisational weakness such as the joiner-mover-leaver procedure is not followed has no CVE and no affected asset, so it cannot be parked in
risks.Vulnerability(see the risks module) either, and the fallback would have been a discarded event with free-text notes.
The cost of the decision is real and is stated here rather than buried : it changes an existing module and it breaks an existing MCP permission contract. See Migration and breaking changes.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
UUID | PK, auto-generated | Unique identifier |
reference |
string | auto-generated, unique, per-type prefix |
NCMAJ-1, NCMIN-1, OBS-1, OA-1, STR-1. Generated by _generate_reference_for_type() from FINDING_REFERENCE_PREFIXES, not by ReferenceGeneratorMixin, because the prefix depends on finding_type and is longer than four characters. |
assessment |
relation | FK -> ComplianceAssessment, optional, SET_NULL
|
Changed. Was required with CASCADE. Now nullable, so a nonconformity can exist without a fabricated audit, and SET_NULL, so deleting an audit no longer destroys the nonconformities it raised. Reverse accessor findings. |
source |
enum | required, default audit
|
New. FindingSource : what surfaced the nonconformity. The default keeps every pre-existing row semantically unchanged. |
incident |
relation | FK -> Incident, optional, SET_NULL
|
New. Set when source = incident. Reverse accessor findings. Note that findings now denotes different things on User, ComplianceAssessment, Requirement and Incident; that is legal but worth knowing when reading code. |
finding_type |
enum | required |
FindingType : major_nc, minor_nc, observation, improvement, strength. Drives the reference prefix and the severity ordering. |
description |
text | required | The nonconformity itself : what was observed, against what requirement |
recommendation |
text | optional, blank default | Recommended treatment. Labelled Auditor recommendation; the label is now source-neutral in the UI. |
evidence |
text | optional, blank default | Evidence presented at the time of the observation |
assessor |
relation | FK -> AUTH_USER_MODEL, optional, PROTECT
|
Changed. Was required. Now null=True, blank=True, with clean() requiring it when source == audit. The column is unchanged; only verbose_name moves from Assessor to Raised by, because the field names whoever raised the nonconformity and that is no longer always an auditor. A PostIncidentReview stamps it with its facilitator when it raises one. |
requirements |
relation | M2M -> Requirement, optional | Requirements the nonconformity bears on. Feeds apply_findings_to_results(). |
effectiveness_reviewed_at |
datetime | New, optional | When the corrective action's effectiveness was reviewed. Clause 10.2 d). |
effectiveness_reviewed_by |
relation |
New, FK -> AUTH_USER_MODEL, optional, SET_NULL
|
Who reviewed it |
effectiveness_verdict |
enum | New, optional, blank default |
EffectivenessVerdict : effective, partially_effective, not_effective. Clause 10.2 f). |
Inherited from BaseModel : workflow_state (default 4-state lifecycle, unchanged), version, created_by, created_at, updated_at, tags, and HistoricalRecords.
| Value | Label |
|---|---|
audit |
Audit |
incident |
Security incident |
management_review |
Management review |
monitoring |
Monitoring |
complaint |
Complaint |
Audit and Monitoring and Complaint already exist as bare msgids in the catalogue, so these labels are declared with pgettext_lazy("finding source", ...) and a matching msgctxt.
| Value | Label |
|---|---|
effective |
Effective |
partially_effective |
Partially effective |
not_effective |
Not effective |
ComplianceActionPlan runs a 7-state Kanban lifecycle that ends at Done. Done proves an action was implemented. It says nothing about whether the nonconformity stopped recurring, which is exactly what clause 10.2 d) review the effectiveness of any corrective action taken asks. Adding an eighth state to the action plan would answer it for action plans only, and a nonconformity can be closed by several action plans or by none. The verdict therefore belongs to the nonconformity.
| ID | Rule |
|---|---|
| RG-FND-01 |
assessment is optional. A Finding with source = audit and no assessment is a data error, not a supported state : clean() requires an assessment when source == audit. |
| RG-FND-02 |
assessor is required when source == audit and optional otherwise, enforced in clean(). The column stays non-null-constrained at database level only in the sense that existing rows are unaffected; the field itself becomes null=True. |
| RG-FND-03 | Deleting a ComplianceAssessment no longer deletes its findings : the FK is SET_NULL. A nonconformity survives the audit that raised it, which is the point of a register. |
| RG-FND-04 |
ComplianceAssessment.apply_findings_to_results() and the assessment's own scoring iterate the reverse accessor self.findings, which by construction never yields a null-assessment row. Incident-born nonconformities therefore never leak into audit-scoped scoring, with no filter to remember and no null guard to write. |
| RG-FND-05 | The management-review nonconformity section is deliberately source-agnostic : it lists every nonconformity regardless of origin, which is what ISO 27001 clause 9.3.2 c) asks for. This is the one place the two populations are meant to meet. |
| RG-FND-06 |
effectiveness_verdict may only be set once the nonconformity has at least one linked ComplianceActionPlan in a reportable state, or an explicit justification for why none was needed. A verdict about the effectiveness of nothing is not a record. |
This is the part to read before merging.
Schema. One migration on compliance : alter assessment (CASCADE -> SET_NULL, null=True), alter assessor (null=True, verbose_name), add source, incident, and the three effectiveness fields. source defaults to audit, so every existing row keeps its exact current meaning and no data migration is needed. The incident FK lands with the m6 app itself, since compliance cannot import incidents before it exists.
A new permission feature. There is no compliance.finding feature today : findings are gated by compliance.assessment.*. Promoting the register to a standalone entity introduces compliance.finding with the usual action set, which requires its own accounts data migration. Without it the feature exists in PERMISSION_REGISTRY, tests pass because the test fixtures seed from the registry, and on a real database it is granted to nobody.
A breaking MCP contract change. list_findings, get_finding, create_finding, update_finding and delete_finding already exist and are gated compliance.assessment.read / .create / .update / .delete. They are re-gated to compliance.finding.*. Any external MCP client holding assessment permissions but not finding permissions loses access at the moment of upgrade. This must be logged as a ### Changed entry in CHANGELOG.md and reflected in docs/mcp-server.md.
New web surfaces. Every finding route in compliance/urls.py is currently nested under an assessment (assessments/<uuid:assessment_pk>/findings/...). A nonconformity with no assessment is unreachable through them, so the register needs standalone list, detail, create, update and delete routes alongside the existing nested ones. The nested routes stay : creating a finding from inside an audit is still the common path.
-
GET /api/v1/compliance/findings/: list, filtersassessment,source,incident,finding_type,effectiveness_verdict,workflow_state -
POST /api/v1/compliance/findings/: create -
GET|PATCH|DELETE /api/v1/compliance/findings/{id}/: retrieve, update, delete -
GET /api/v1/compliance/findings/{id}/history/: audit trail
list_findings, get_finding, create_finding, update_finding, delete_finding, batch_create_findings. Writable fields gain source, incident_id, effectiveness_reviewed_at, effectiveness_reviewed_by_id and effectiveness_verdict; assessment_id becomes optional. Read-only *_name companions follow the house convention : assessment_name, incident_reference, assessor_name, effectiveness_reviewed_by_name.
| Codename | Grants |
|---|---|
compliance.finding.read |
View the nonconformity register and any finding |
compliance.finding.create |
Raise a nonconformity |
compliance.finding.update |
Edit a nonconformity, record an effectiveness verdict |
compliance.finding.delete |
Delete a nonconformity while its lifecycle step allows it |
The six system roles receive these through the existing suffix lambdas in accounts/constants.py, unchanged.
- ComplianceAssessment : the audit that raises most nonconformities
- ComplianceActionPlan : the corrective actions that treat them
- Requirement : what a nonconformity is measured against
- PostIncidentReview : raises incident-born nonconformities
- Module README : cross-cutting business rules, API conventions and permissions
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)