model-wtf 1.0.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_wtf-1.0.0rc1/PKG-INFO +759 -0
- model_wtf-1.0.0rc1/README.md +726 -0
- model_wtf-1.0.0rc1/pyproject.toml +174 -0
- model_wtf-1.0.0rc1/pyproject.toml.orig +149 -0
- model_wtf-1.0.0rc1/src/model_wtf/__init__.py +0 -0
- model_wtf-1.0.0rc1/src/model_wtf/__main__.py +10 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/challenger.md +62 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/dispatcher.md +19 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/grouper.md +68 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/reviewer.md +93 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/threat_dispatcher.md +15 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/threat_reviewer.md +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/topic_dispatcher.md +21 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/topic_reviewer.md +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/tp_dispatcher.md +19 -0
- model_wtf-1.0.0rc1/src/model_wtf/agents/tp_reviewer.md +199 -0
- model_wtf-1.0.0rc1/src/model_wtf/cli.py +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/__init__.py +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/activities.py +449 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/auto_review.py +1179 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/check.py +505 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/cli.py +486 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/codeowners.py +186 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/data.py +937 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/data_cli.py +646 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/declarations.py +208 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/discovery.py +260 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/exit_codes.py +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/findings.py +135 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/flows.py +475 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/flows_cli.py +180 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/gate.py +522 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/init_cmd.py +497 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/knowledge.py +503 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/mcp_server.py +2327 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/ops.py +376 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/options.py +36 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/render.py +467 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/report.py +402 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/review.py +335 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/rights.py +1009 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/schemas.py +131 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/severity.py +369 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/stamps.py +284 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/stores.py +306 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/stores_cli.py +190 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/threats.py +1204 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/threats_cli.py +833 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/threats_gen.py +158 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/touchpoints.py +1179 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/touchpoints_cli.py +1067 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/workspace.py +192 -0
- model_wtf-1.0.0rc1/src/model_wtf/compliance/yaml_io.py +224 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/__init__.py +0 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/cache.py +151 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/django_models.py +357 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/django_touchpoints.py +814 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/runner.py +385 -0
- model_wtf-1.0.0rc1/src/model_wtf/introspect/sveltekit_touchpoints.mjs +282 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/adequacy.yaml +50 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/behavioural.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/biometric.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/connection.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/contact.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/content.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/credentials.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/criminal.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/financial.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/health.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/identity.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/location.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/professional.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/special_other.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/technical.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/address.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/behavioural.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/birth_date.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/connection.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/credential.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/email.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/fallback.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/file.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/file_path.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/financial.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/health.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/identifier.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/json.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/location.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/name.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/phone.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/primary_key.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/relation.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/special.yaml +7 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/technical.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/admin.LogEntry.yaml +21 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Group.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Group_permissions.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Permission.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User.yaml +14 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User_groups.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User_user_permissions.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/contenttypes.ContentType.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateEvent.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateJob.yaml +12 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinatePeriodicDefer.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateWorker.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/sessions.Session.yaml +19 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/taggit.Tag.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/taggit.TaggedItem.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.Admin.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.EditingSession.yaml +11 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.FormState.yaml +18 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Collection.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CollectionViewRestriction.yaml +4 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CollectionViewRestriction_groups.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Comment.yaml +15 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CommentReply.yaml +15 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupApprovalTask.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupApprovalTask_groups.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupCollectionPermission.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupPagePermission.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupSitePermission.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Locale.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.ModelLogEntry.yaml +19 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Page.yaml +27 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageLogEntry.yaml +19 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageSubscription.yaml +11 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageViewRestriction.yaml +4 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageViewRestriction_groups.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.ReferenceIndex.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Revision.yaml +22 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Site.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Task.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.TaskState.yaml +12 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.UploadedFile.yaml +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Workflow.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowContentType.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowPage.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowState.yaml +12 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowTask.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtaildocs.Document.yaml +21 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailembeds.Embed.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailforms.FormSubmission.yaml +13 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailimages.Image.yaml +21 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailimages.Rendition.yaml +6 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailredirects.Redirect.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailsearch.IndexEntry.yaml +17 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailsearch.Query.yaml +4 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailusers.UserProfile.yaml +20 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wailer.Email.yaml +17 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wailer.Sms.yaml +17 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.CheckIn.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Control.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Control_depends_on.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Cron.yaml +2 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Incident.yaml +6 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.RunRecord.yaml +8 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/confidential.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/internal.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/personal.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/public.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/special.yaml +5 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA01.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA02.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA03.yaml +61 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA04.yaml +53 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC01.yaml +39 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC02.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC03.yaml +48 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC04.yaml +58 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC05.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC06.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC07.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC08.yaml +35 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC09.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC10.yaml +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC11.yaml +40 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC12.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC13.yaml +32 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC14.yaml +37 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC15.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC16.yaml +31 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC17.yaml +27 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC18.yaml +28 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC19.yaml +31 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC20.yaml +32 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC21.yaml +49 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC22.yaml +23 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC23.yaml +22 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC24.yaml +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/API01.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/API02.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR01.yaml +47 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR02.yaml +58 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR03.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR04.yaml +39 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR05.yaml +31 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR06.yaml +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR07.yaml +58 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR08.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE01.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE02.yaml +47 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE03.yaml +28 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE04.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO01.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO02.yaml +40 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO03.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO04.yaml +42 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO05.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DR01.yaml +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS01.yaml +40 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS02.yaml +27 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS03.yaml +62 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS04.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS05.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS06.yaml +28 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA01.yaml +48 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA02.yaml +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA03.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA04.yaml +32 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP01.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP02.yaml +37 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP03.yaml +36 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP04.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP05.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP06.yaml +42 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP07.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP08.yaml +35 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP09.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP10.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP11.yaml +39 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP12.yaml +36 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP13.yaml +47 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP14.yaml +35 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP15.yaml +44 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP16.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP17.yaml +38 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP18.yaml +46 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP19.yaml +26 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP20.yaml +43 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP21.yaml +26 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP22.yaml +28 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP23.yaml +47 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP24.yaml +46 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP25.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP26.yaml +26 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP27.yaml +28 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP28.yaml +44 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP29.yaml +34 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP30.yaml +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP31.yaml +35 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP32.yaml +33 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP33.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP34.yaml +29 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP35.yaml +32 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP36.yaml +37 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP37.yaml +43 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP38.yaml +49 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP39.yaml +53 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP40.yaml +45 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP41.yaml +36 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LB01.yaml +36 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM01.yaml +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM02.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM03.yaml +26 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM04.yaml +24 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM05.yaml +26 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM06.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM07.yaml +23 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM08.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM09.yaml +25 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC01.yaml +68 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC02.yaml +35 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC03.yaml +68 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC04.yaml +54 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC05.yaml +46 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_actors.yaml +30 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_mapping.yaml +176 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_rules.yaml +218 -0
- model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_topics.yaml +99 -0
- model_wtf-1.0.0rc1/src/model_wtf/opencode.py +605 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: model-wtf
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Compliance as code for Model W projects: data inventory, processing register, threat model, and a gate on every pull request
|
|
5
|
+
Keywords: gdpr,compliance,threat-model,django,sveltekit,security
|
|
6
|
+
Author: Rémy Sanchez
|
|
7
|
+
Author-email: Rémy Sanchez <remy.sanchez@hyperthese.net>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Legal Industry
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: rich-click
|
|
21
|
+
Requires-Dist: click
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Requires-Dist: pyyaml
|
|
24
|
+
Requires-Dist: pydantic>=2.13.5
|
|
25
|
+
Requires-Dist: ruamel-yaml>=0.19.1
|
|
26
|
+
Requires-Dist: mcp>=1.2
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Project-URL: Documentation, https://modelw.github.io/wtf/
|
|
29
|
+
Project-URL: Source, https://github.com/ModelW/wtf
|
|
30
|
+
Project-URL: Changelog, https://github.com/ModelW/wtf/blob/develop/CHANGELOG.md
|
|
31
|
+
Project-URL: Issues, https://github.com/ModelW/wtf/issues
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# `model-wtf`
|
|
35
|
+
|
|
36
|
+
The Model W Transformation Facilitator is a CLI tool that facilitates Model W
|
|
37
|
+
compliance of a given Git repo.
|
|
38
|
+
|
|
39
|
+
It maintains a compliance-oriented model of the application — its data,
|
|
40
|
+
components and flows — declared in YAML under `compliance/` folders next to
|
|
41
|
+
the code. That model feeds static analysis and code review, and derived
|
|
42
|
+
documents such as the GDPR Art. 30 registry or the threat model.
|
|
43
|
+
|
|
44
|
+
**Documentation: <https://modelw.github.io/wtf/>** — the model, task-shaped
|
|
45
|
+
guides (adding it to a project, a PR that failed the gate, answering a
|
|
46
|
+
finding, reading the register) and the generated reference (CLI, file
|
|
47
|
+
schemas, threat catalogue). What follows is the same material in one file.
|
|
48
|
+
|
|
49
|
+
## Compliance
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
uv run model-wtf compliance init [--name X] [--controller-name X --controller-country CC]
|
|
53
|
+
[--processor-name X --processor-country CC | --no-processor]
|
|
54
|
+
[--codeowners | --no-codeowners] [--owner-dpo @org/team] [--owner-ciso @org/team]
|
|
55
|
+
uv run model-wtf compliance check [--strict] [--allow-todo] [--todo] [-v] [--format text|json|github] [--root PATH]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`init` scaffolds the repo-root `compliance/` folder (`app.yaml`, one
|
|
59
|
+
`parties/<id>.yaml` per organisation, a README), adds a `compliance:` block to
|
|
60
|
+
every image of `snow.yml` (guessing the discovery backend from the code) and
|
|
61
|
+
creates the per-unit folders. It never
|
|
62
|
+
overwrites anything; re-run it to add what is missing. Values left for a human
|
|
63
|
+
are written as the YAML tag `!todo`. The processor defaults to
|
|
64
|
+
`default_processor: {name, country, address, email}` from
|
|
65
|
+
`~/.config/model-wtf/config.yml`.
|
|
66
|
+
|
|
67
|
+
When `origin` is on GitHub, `init` also writes a managed block into
|
|
68
|
+
`.github/CODEOWNERS` (between `# model-wtf compliance (managed)` markers,
|
|
69
|
+
hand-written rules untouched, re-runs rewrite it in place): the register
|
|
70
|
+
(`activities/`, `parties/`, `data/`, `data.lock.yaml`) to the DPO team, the
|
|
71
|
+
posture (`stores/`, `findings.lock.yaml`, `snow.yml`) to the CISO team,
|
|
72
|
+
`app.yaml` and `touchpoints/` to both. Teams default to `@<org>/dpo` and
|
|
73
|
+
`@<org>/ciso`; `--owner-dpo`/`--owner-ciso` override and are recorded as
|
|
74
|
+
`owners:` in `app.yaml`, which decides on later runs. `--codeowners` makes
|
|
75
|
+
a missing owner an error; `--no-codeowners` skips. `check` prints an info
|
|
76
|
+
line when a compliance folder (a unit added later) has no owner and a
|
|
77
|
+
`CODEOWNERS` exists.
|
|
78
|
+
|
|
79
|
+
`check` is the to-do list. It discovers the units, validates every
|
|
80
|
+
declaration file against its schema (pydantic; unknown keys are errors) and
|
|
81
|
+
groups what it finds by the kind of work it asks for:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Errors — fix the files exit 3
|
|
85
|
+
Missing — non-compliant code or process, to build exit 1, never ignorable
|
|
86
|
+
Todo — questions only a human can answer exit 1 (--allow-todo → 0)
|
|
87
|
+
Review — run the agents, or decide exit 1
|
|
88
|
+
Info (-v for the details)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
One line per thing to do, with the command that resolves it after an arrow;
|
|
92
|
+
marker findings are folded per file (`parties/fah.yaml: address, email`).
|
|
93
|
+
`check --todo` prints only the open questions, one per line with the
|
|
94
|
+
question the field asks, so the list can be handed to whoever holds the
|
|
95
|
+
answers. `--format json` groups under `sections` with a stable `subject`
|
|
96
|
+
per entry (`app.yaml#description`, `api:data`) that a gate can diff between
|
|
97
|
+
runs; `--format github` maps Errors/Missing to `error`, Todo/Review to
|
|
98
|
+
`warning`, Info to `notice`.
|
|
99
|
+
|
|
100
|
+
Two YAML tags mark a value deliberately left open, both with an optional
|
|
101
|
+
note: `!todo` (the analysis has not been conducted) and `!missing "no purge
|
|
102
|
+
task, see FAH-210"` (it has, and the code or process is not there — an
|
|
103
|
+
established non-compliance, which always fails the gate).
|
|
104
|
+
|
|
105
|
+
### Files
|
|
106
|
+
|
|
107
|
+
- `compliance/app.yaml` — `name`, `description`, `controller` (party id, the
|
|
108
|
+
client), optional `processor` (party id, the agency), `large_scale`
|
|
109
|
+
(Art. 35(3)(b); absent means no: a DPIA is then only required for
|
|
110
|
+
special-category data; `!todo` asks the question once) and `owners`
|
|
111
|
+
(`dpo`/`ciso` GitHub teams for `CODEOWNERS`).
|
|
112
|
+
- `compliance/parties/<id>.yaml` — `name`, `country` (ISO alpha-2),
|
|
113
|
+
`address`, `email`; optional `phone`, `website`, `hosts` (API hostnames the
|
|
114
|
+
code calls when they differ from the website, e.g. `api.hubapi.com`: a
|
|
115
|
+
call to one is a transfer to this party), `registration`, `dpa`
|
|
116
|
+
(where the processing agreement lives), `safeguard`/`dpf_certified`,
|
|
117
|
+
`dpo` and `representative` contact blocks. A party is role-less:
|
|
118
|
+
controller, processor or recipient is decided per processing activity. A
|
|
119
|
+
party nothing refers to (no transfer, no role, no `recipients`) is a Todo.
|
|
120
|
+
|
|
121
|
+
### Unit discovery
|
|
122
|
+
|
|
123
|
+
Units are read from `snow.yml` at the repo root: every `images[]` entry that
|
|
124
|
+
carries a `compliance:` block is a unit. `discover` names the discovery backend
|
|
125
|
+
for that codebase (`django`, `sveltekit`, `none`); the compliance folder is
|
|
126
|
+
`compliance/` next to the image's Dockerfile unless `dir` (relative to the
|
|
127
|
+
build context) says otherwise. An image without `compliance` produces a
|
|
128
|
+
warning (an error under `--strict`).
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
images:
|
|
132
|
+
- id: api
|
|
133
|
+
context: api # Dockerfile at api/Dockerfile
|
|
134
|
+
compliance:
|
|
135
|
+
discover: django # -> api/compliance
|
|
136
|
+
- id: front
|
|
137
|
+
context: .
|
|
138
|
+
dockerfile: front/Dockerfile
|
|
139
|
+
compliance:
|
|
140
|
+
discover: sveltekit # -> front/compliance
|
|
141
|
+
- id: docs
|
|
142
|
+
context: .
|
|
143
|
+
compliance:
|
|
144
|
+
discover: none
|
|
145
|
+
dir: docs/compliance # -> docs/compliance
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Repos not deployed through Snow can use `.model-wtf.yml` instead
|
|
149
|
+
(`units: [{id, context, dockerfile, compliance}]`). When both files exist
|
|
150
|
+
`snow.yml` wins.
|
|
151
|
+
|
|
152
|
+
The repo-root `compliance/` folder is always loaded as the _shared_ scope
|
|
153
|
+
(controller, actors, assumptions, recipients).
|
|
154
|
+
|
|
155
|
+
### Data inventory
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
uv run model-wtf compliance data list [--unit ID] [--format table|json]
|
|
159
|
+
uv run model-wtf compliance data rules
|
|
160
|
+
uv run model-wtf compliance data override <unit>:<app.Model.field> [--pii|--no-pii] [--sensitivity L] [--category C] [--reason TEXT]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every Django model field of every `discover: django` unit is inventoried
|
|
164
|
+
live — model-wtf detects the unit's interpreter (uv, Poetry, `.venv`,
|
|
165
|
+
`MODEL_WTF_PYTHON`) and its `DJANGO_SETTINGS_MODULE` (env, `manage.py`,
|
|
166
|
+
`[tool.model-wtf] django_settings`), then pipes its own stdlib-only
|
|
167
|
+
introspection script into it. Nothing generated is written to disk.
|
|
168
|
+
|
|
169
|
+
Each field gets three classifications from the built-in rules
|
|
170
|
+
(`model_wtf/knowledge/data_rules/`, first match by ascending priority):
|
|
171
|
+
|
|
172
|
+
- `pii` — personal data or not;
|
|
173
|
+
- `sensitivity` — ordinal: `public < internal < personal < confidential < special`,
|
|
174
|
+
each level carrying a DPIA hint (`never`, `large_scale`, `always`);
|
|
175
|
+
- `category` — nominal, what the Art. 30 register will print (`identity`,
|
|
176
|
+
`contact`, `financial`, `connection`, `location`, `behavioural`, `content`,
|
|
177
|
+
`credentials`, `health`, `biometric`, `special_other`, `criminal`,
|
|
178
|
+
`professional`, `technical`).
|
|
179
|
+
|
|
180
|
+
`JSONField`s are presumed to hold personal data (`confidential`, `content`)
|
|
181
|
+
until a review says otherwise; unrecognised plain fields default to
|
|
182
|
+
`technical` and rely on the review to be promoted.
|
|
183
|
+
|
|
184
|
+
### JSON-like columns hold *contents*
|
|
185
|
+
|
|
186
|
+
A `JSONField` / `ArrayField` / `HStoreField` (not Wagtail's `StreamField`,
|
|
187
|
+
which is CMS content) is a container: one triple cannot describe a blob
|
|
188
|
+
holding a name, an address and an IBAN. Its file declares what it holds,
|
|
189
|
+
one entry per **kind** of information (identifiers, not JSON paths):
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
contents:
|
|
193
|
+
customer_name: {pii: true, sensitivity: personal, category: identity}
|
|
194
|
+
iban: {pii: true, sensitivity: confidential, category: financial}
|
|
195
|
+
utm_campaign: {pii: false, sensitivity: internal, category: technical}
|
|
196
|
+
unknown_contents: none # none | possible | likely — is the list exhaustive?
|
|
197
|
+
reason: written in orders/services.py:88-104 and checkout/serializers.py:41
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Each entry becomes a row `<app.Model.field>@json.<name>` reviewed and
|
|
201
|
+
overridable on its own; the column's verdict is **derived** (`pii` = any,
|
|
202
|
+
`sensitivity` = max, `category` = the set as `financial+identity+technical`,
|
|
203
|
+
DPIA = max). `unknown_contents: none` replaces the rule's presumption,
|
|
204
|
+
`possible` keeps a `json-unknown-contents` warning, `likely` folds the
|
|
205
|
+
presumption back in (so `contents: {}` + `likely` = "opaque, treat as
|
|
206
|
+
personal"). `data contents <unit:id> name=yes,personal,contact ... [--unknown none]
|
|
207
|
+
[--reason ...]` writes the file; the auto-review never closes a JSON-like
|
|
208
|
+
field with a bare `ok`: it follows the write sites `data_model` lists and
|
|
209
|
+
declares the contents itself.
|
|
210
|
+
|
|
211
|
+
Humans correct the rules with `<unit>/compliance/data/<app.Model.field>.yaml`
|
|
212
|
+
(any subset of `pii` / `sensitivity` / `category` / `store`, plus a `reason`),
|
|
213
|
+
and add data the ORM does not know with a complete manual item
|
|
214
|
+
(`description`, `pii`, `sensitivity`, `category`, optional `store`) under any
|
|
215
|
+
other id.
|
|
216
|
+
|
|
217
|
+
`init --custom-sensitivity` / `--custom-categories` copy the built-in scale
|
|
218
|
+
or category list into `compliance/sensitivity/` / `compliance/categories/`
|
|
219
|
+
for editing; a renamed entry declares `replaces: [<built-in id>]` so the
|
|
220
|
+
rules still resolve, and `check` verifies every built-in id is covered once.
|
|
221
|
+
|
|
222
|
+
### Review
|
|
223
|
+
|
|
224
|
+
The inventory is virtual, so what has been looked at is tracked in
|
|
225
|
+
`<unit>/compliance/data.lock.yaml` (fingerprint of the field facts *and* of
|
|
226
|
+
the verdict, who, when, note). `data list` shows a `Review` column
|
|
227
|
+
(`pending:new`, `pending:changed`, `reviewed`, `override`, `known`) and
|
|
228
|
+
`--pending` filters on it; `check` fails with exit 1 while
|
|
229
|
+
anything is pending.
|
|
230
|
+
|
|
231
|
+
- `data reviewed <unit>:<id>... [--note TEXT]` — a human confirms the current
|
|
232
|
+
classification.
|
|
233
|
+
- `data override …` also marks the item reviewed.
|
|
234
|
+
- Third-party models are reviewed like the project's own: what a task queue
|
|
235
|
+
or a user table holds is this project's data. What model-wtf already knows
|
|
236
|
+
about them lives in `knowledge/library/<app.Model>.yaml`: per field a
|
|
237
|
+
default verdict with `fixed: true` when the framework fixes the meaning
|
|
238
|
+
(`auth.User.password` — source/status `known`, nothing to review) or
|
|
239
|
+
`fixed: false` when it depends on the project (`ProcrastinateJob.args`,
|
|
240
|
+
`Session.session_data`, `FormSubmission.form_data` — source `library`,
|
|
241
|
+
status `pending:assumed`). Assumed models carry an `assumption` and a
|
|
242
|
+
`check`: what we take for granted and what to look at in *this* project;
|
|
243
|
+
`data list` prints the assumption under the row (`--assumed` filters on
|
|
244
|
+
them), `data_model` shows it to the agent (which must do the check before
|
|
245
|
+
confirming); `check` only counts them in the pending breakdown
|
|
246
|
+
(`37 data item(s) pending (12 new, 9 assumed, 16 contents)`).
|
|
247
|
+
`fields_default` covers unlisted columns of tables that are technical
|
|
248
|
+
through and through.
|
|
249
|
+
- Every file field also yields `<field>@files.content`: the bytes in the
|
|
250
|
+
storage behind the column, classified on their own.
|
|
251
|
+
|
|
252
|
+
### Stores
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
uv run model-wtf compliance stores list [--unit ID] [--all] [--format table|json]
|
|
256
|
+
uv run model-wtf compliance stores explain <unit>:<slug>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Where the data lives is an item with a slug, and every data row references one
|
|
260
|
+
in its `Store` column. A store is a slug, a `type` and a conceptual `backend`
|
|
261
|
+
(`postgresql`, `redis`, `s3`, `filesystem`, ...) — nothing environmental:
|
|
262
|
+
hosts, bucket names and credentials belong to a deployment, not to the model
|
|
263
|
+
of the application. Stores are read from the Django settings, so there is
|
|
264
|
+
nothing to write for the common case: `DATABASES[alias]` → `db-<alias>` (rows
|
|
265
|
+
follow the router), `CACHES` → `cache-<alias>`, `STORAGES` → `files-<alias>`
|
|
266
|
+
(`bucket` when the backend is S3/GCS/Azure, else `filesystem`; `staticfiles`
|
|
267
|
+
skipped), a field's own `storage=` → `files-<app.Model.field>`,
|
|
268
|
+
`CELERY_BROKER_URL` → `queue-celery`, `WAGTAILSEARCH_BACKENDS` →
|
|
269
|
+
`search-<alias>`.
|
|
270
|
+
|
|
271
|
+
Optional `<unit>/compliance/stores/<slug>.yaml` files can override facts of an
|
|
272
|
+
introspected store (`backend`, `name`, `provider`, `location` as a region or
|
|
273
|
+
country, `retention`, `description`), declare a store the settings do not show
|
|
274
|
+
(`type: external`, `browser`, ... — `type` is then mandatory), or hide one
|
|
275
|
+
with `ignore: true`.
|
|
276
|
+
`check` reports `store-unknown` / `store-ignored-referenced` for data rows
|
|
277
|
+
naming a slug that does not exist or is hidden, and `store-orphan` for a
|
|
278
|
+
manual store file without `type`. `data override … --store <slug>` moves a
|
|
279
|
+
row to another store.
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
uv run model-wtf compliance data auto-review [--unit ID] [--base REF] [--batch 8] [--workers 16] [--max-rounds 20] [--max-tokens N] [--model provider/model] [--dry-run]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Runs an OpenCode agent on OpenRouter (`openrouter/openrouter/auto` by default;
|
|
286
|
+
`OPENROUTER_API_KEY` required) until nothing is pending. The instance is
|
|
287
|
+
sandboxed (`model_wtf/opencode.py`): throwaway `HOME`/XDG tree, generated
|
|
288
|
+
config via `OPENCODE_CONFIG`, `--pure`, whitelisted environment, deny-all
|
|
289
|
+
permissions except read/glob/grep inside the repository and its interpreters'
|
|
290
|
+
import roots, our MCP server as the only write path; repository-level
|
|
291
|
+
`opencode.json` / `.opencode/` / `AGENTS.md` have no effect. Work is
|
|
292
|
+
dispatched one **model** at a time: `data_pending` → `data_model` (field
|
|
293
|
+
table + class source + JSON write sites) → `data_review_model` (all decisions
|
|
294
|
+
in one call), which keeps each subagent session small enough for flash-class
|
|
295
|
+
models. `--base REF` also re-dispatches models whose file changed since `REF`.
|
|
296
|
+
|
|
297
|
+
### Touchpoints
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
uv run model-wtf compliance touchpoints list [--unit ID] [--pending] [--all] [--format json]
|
|
301
|
+
uv run model-wtf compliance touchpoints show <unit>:<id>
|
|
302
|
+
uv run model-wtf compliance touchpoints set-data <unit>:<id> [<unit>:<item>[=read|write]...] [--add|--remove] [--ignore] [--note ...]
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Vocabulary, kept clear of pytm's: a **unit** is a running component (pytm
|
|
306
|
+
*Process*); a **touchpoint** is one of its entry points through which data
|
|
307
|
+
flows (pytm *Dataflows*); an **activity** is a GDPR processing activity. The
|
|
308
|
+
word "process" is not used.
|
|
309
|
+
|
|
310
|
+
Touchpoints are introspected, never written: Django URL patterns (id = route
|
|
311
|
+
name, or `METHOD /path`; Ninja endpoints by operation id with their
|
|
312
|
+
request/response schemas flattened from the API's own OpenAPI document, DRF
|
|
313
|
+
serializers, `FormView` fields, auth classes), Procrastinate/Celery tasks
|
|
314
|
+
(`task:<name>`, signature, periodic flag, tasks they defer) and admin screens
|
|
315
|
+
(`admin:<app.Model>`, the fields staff see). SvelteKit units run `svelte-kit
|
|
316
|
+
sync` and read the generated `$types.d.ts` with the project's own TypeScript:
|
|
317
|
+
id = route ID, `RouteParams`, `PageData`/`ActionData` shapes, form field
|
|
318
|
+
names, and the generated-API-client operations the route calls — which link
|
|
319
|
+
to the Django touchpoints by operation id (`calls`). Plumbing (health checks,
|
|
320
|
+
OpenAPI documents, the admin's own URL patterns) is ignored by default.
|
|
321
|
+
|
|
322
|
+
The optional manifest `<unit>/compliance/touchpoints/<slug>.yaml` declares
|
|
323
|
+
what the touchpoint **does** to data, with a closed vocabulary of **facts**
|
|
324
|
+
(`src/model_wtf/compliance/ops.py`): each `data:` entry is a ref (`@json`/
|
|
325
|
+
`@files` rows allowed, `unit:app.Model.*` for a whole model) and its ops —
|
|
326
|
+
a bare `- unit:app.Model.field` is a `read`, `- ref: create`, `- ref: [create,
|
|
327
|
+
read]`, `- ref: {delete: {mode: anonymise}}`, `- ref: {retention_purge:
|
|
328
|
+
{after: settings.ANONYMOUS_ADDRESS_MAX_AGE, since: last use, when: anonymous
|
|
329
|
+
only}}`. Verbs: `create[{consent_for}]`, `read`, `update`, `delete[{mode}]`,
|
|
330
|
+
`retention_purge{after (duration or setting name), since, when?}`,
|
|
331
|
+
`portability{format}`, `consent_withdraw{for}`; each verb takes only its own
|
|
332
|
+
metadata. No legal verb: a person changing their own address is an `update`,
|
|
333
|
+
deleting it a `delete` — what that means for their rights follows from the
|
|
334
|
+
touchpoint's **scope** (`scope: subject | staff | public | system`, inferred
|
|
335
|
+
from auth classes, `request.user` in the body and admin namespaces, or
|
|
336
|
+
declared in the manifest). `write`, `rectify`, `access`, `erase`, `object`,
|
|
337
|
+
`restrict` still load, folded onto the fact they imply with an
|
|
338
|
+
`op-ambiguous` warning. `transfers:`
|
|
339
|
+
lists what leaves to another organisation's API — `- {party: mapbox, data:
|
|
340
|
+
[...], purpose: ...}`, the party being a `compliance/parties/` id, which is
|
|
341
|
+
where the register's recipients come from (`exporting:` still loads, with a
|
|
342
|
+
deprecation warning); plus `ignore`, `note`. The project's own database,
|
|
343
|
+
file storage, cache and queue are *stores*, not transfers, whoever hosts
|
|
344
|
+
them: hosting is a separate layer, taken as adequate here. Every inventory item the code
|
|
345
|
+
touches is listed, personal or not: the register filters on `pii`
|
|
346
|
+
downstream, the data-flow model needs all of it. A touchpoint is
|
|
347
|
+
**pending** until it has a `data` key — an explicit `[]` means "touches no
|
|
348
|
+
inventory item, checked". `check` reports `touchpoint-pending` and
|
|
349
|
+
`touchpoint-orphan` (handles personal data, belongs to no activity), both
|
|
350
|
+
exit 1, and `data-unreferenced` as information.
|
|
351
|
+
|
|
352
|
+
Introspection pre-fills **likely ops** (`touchpoints show` → "likely ops"):
|
|
353
|
+
HTTP method (`POST` → create, `PUT/PATCH` → update|rectify, `DELETE` →
|
|
354
|
+
delete|erase), Django admin permissions (`has_*_permission` overrides,
|
|
355
|
+
`readonly_fields`, `list_display`), task names (`purge|clean|expire`,
|
|
356
|
+
`anonymi[sz]e|erase|gdpr`, `export`) and task bodies (`.delete()`,
|
|
357
|
+
`.update()`, `timedelta(days=30)` as an `after` hint), SvelteKit handlers and
|
|
358
|
+
action names. The reviewer confirms them against the code. `data why`
|
|
359
|
+
prints each item's lifecycle from the ops: *created by api:signup, read by
|
|
360
|
+
6, rectified by admin:people.User (by staff), never erased, purged by
|
|
361
|
+
api:task:cart.purge (after days 30, ...), sent to mapbox*.
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
uv run model-wtf compliance touchpoints auto-review [--unit ID] [--batch 8] [--workers 16] [--max-rounds 20] [--group/--no-group] [--group-only] [--model ...] [--max-tokens N]
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Same sandboxed OpenCode loop as `data auto-review` (`--workers` sessions run in
|
|
368
|
+
parallel each round, each on its own shard of the pending list), two passes. **Pass 1**,
|
|
369
|
+
one touchpoint per subagent session: `touchpoint_show` gives the code
|
|
370
|
+
location, the schemas and what the API operations it calls already declare;
|
|
371
|
+
the reviewer reads the view/task/route, resolves items with `data_search`
|
|
372
|
+
(never typing an id it did not see), creates a **manual item** with
|
|
373
|
+
`data_add_manual` for personal data the ORM has no row for — transient
|
|
374
|
+
(a card number forwarded to the PSP, a position sent to a geocoder, a search
|
|
375
|
+
query) or kept outside the ORM; processing counts even without storage,
|
|
376
|
+
while non-personal transient values are not tracked — and closes
|
|
377
|
+
with one `touchpoint_set_data` call citing file:line (`[]` = touches nothing
|
|
378
|
+
personal). **Pass 2**, once nothing is pending (or right away with
|
|
379
|
+
`--group-only`): a single session reads `activities_graph` — every
|
|
380
|
+
PII-touching touchpoint with its categories, `calls`/`defers` edges and
|
|
381
|
+
current activity — and follows the chains front → api → task to
|
|
382
|
+
`activity_create` / `activity_add_touchpoints`; `legal_basis` only when
|
|
383
|
+
evident, `retention` and the rest stay `!todo` for a human, existing
|
|
384
|
+
activities are never emptied.
|
|
385
|
+
|
|
386
|
+
### Flows
|
|
387
|
+
|
|
388
|
+
Data, touchpoints and activities say what exists, who touches it and why.
|
|
389
|
+
**Flows** say where it goes: one per edge along which items move, named
|
|
390
|
+
`source->sink`, with a **kind** decided from its ends and a **status**:
|
|
391
|
+
|
|
392
|
+
| kind | ends | status |
|
|
393
|
+
|------------|---------------------------------------|---------------------------|
|
|
394
|
+
| `request` | an actor and a touchpoint | derived (shapes) |
|
|
395
|
+
| `store` | a touchpoint and a project store | declared (ops) |
|
|
396
|
+
| `transfer` | a touchpoint and a party | declared (`transfers`) |
|
|
397
|
+
| `call` | a front route and an API operation | derived (introspection) |
|
|
398
|
+
| `defer` | a touchpoint and a background task | derived |
|
|
399
|
+
| any | found by a reviewer, absent from the model | **undeclared** — a gap |
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
uv run model-wtf compliance flows list [--unit api] [--element api:listRestaurants] [--kind transfer] [--status undeclared] [--format json]
|
|
403
|
+
uv run model-wtf compliance flows show api:listRestaurants->party:mapbox # items, and the threat cells on it
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The threat reviewers get the same inventory in words through the `flows`
|
|
407
|
+
tool (and inside `threat_topic`): *sends geo.Address.position to
|
|
408
|
+
party:mapbox (US) — declared transfer, safeguarded: sending it there is the
|
|
409
|
+
intended use*; *create/read Cart rows on api:db-default*; *exchanges 38
|
|
410
|
+
items with anonymous callers*. A declared, safeguarded transfer is not a
|
|
411
|
+
leak, and the reviewer no longer reconstructs the flows from the code and
|
|
412
|
+
calls one. What the code sends somewhere **not on the list** is reported
|
|
413
|
+
with `flow_report(element, sink, data, note)`: the manifest gets an
|
|
414
|
+
`undeclared:` entry, `check` shows a `flow-undeclared` finding (Missing) and
|
|
415
|
+
the touchpoint is pending again — the declaration is incomplete. Declaring
|
|
416
|
+
the transfer (party first) closes it; the transfer's own threats then
|
|
417
|
+
follow the `declared_transfer` rule.
|
|
418
|
+
|
|
419
|
+
Flow-only threats (DS06, DR01, AC22…) are stamped per flow: `DS06@actor:public`
|
|
420
|
+
for the response, `DS06@api:db-default` for the store side. A bare `DS06` on
|
|
421
|
+
the touchpoint is refused while several flows carry the open cell, with the
|
|
422
|
+
keys to use; it is accepted when only one does.
|
|
423
|
+
|
|
424
|
+
### Activities
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
uv run model-wtf compliance activities list [--format json]
|
|
428
|
+
uv run model-wtf compliance activities explain <slug>
|
|
429
|
+
uv run model-wtf compliance activities create <slug> [--name] [--purpose] [--legal-basis] [--touchpoint unit:id]... [--subject]... [--recipient]... [--retention]
|
|
430
|
+
uv run model-wtf compliance activities add <slug> <unit:id>...
|
|
431
|
+
uv run model-wtf compliance data why <unit:id>... [--model unit:app.Model] [--manifests] [--format json]
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`compliance/activities/<slug>.yaml` (repository root, activities span units)
|
|
435
|
+
is the Art. 30 row: `name`, `purpose`, `legal_basis` (`consent | contract |
|
|
436
|
+
legal_obligation | vital_interests | public_task | legitimate_interests`, or
|
|
437
|
+
`no_pii` — a claim that the activity handles no personal item, verified at
|
|
438
|
+
every check: `no-pii-violated` otherwise), `data_subjects`, `touchpoints`,
|
|
439
|
+
`recipients` (party ids), `controller`/`processor` (default: `app.yaml`'s);
|
|
440
|
+
`consent: {record: <ref>, granularity: separate|bundled}` for consent-based
|
|
441
|
+
ones (the stored proof, created with `create: {consent_for: <slug>}`),
|
|
442
|
+
`interest` for legitimate interests (the balancing test), `basis_note` when
|
|
443
|
+
two bases compete, `dpia_reference` when the derived trigger fires. Any of
|
|
444
|
+
them may be `!todo` or `!missing "why"`. Everything else is **derived** from
|
|
445
|
+
the touchpoints: the data items, hence categories, stores, maximum
|
|
446
|
+
sensitivity, DPIA trigger, units, recipients and the ops per item. Retention
|
|
447
|
+
is not a field: the policy is the `retention_purge` op in the code.
|
|
448
|
+
|
|
449
|
+
### Rights coverage
|
|
450
|
+
|
|
451
|
+
Nothing about rights is written on activities. Touchpoints state what the
|
|
452
|
+
code does (ops), data items state what is true of the data regardless of
|
|
453
|
+
code, activities carry purpose and basis; `check` derives, **per personal
|
|
454
|
+
item in every activity that handles it**, whether each right is served
|
|
455
|
+
(`src/model_wtf/compliance/rights.py`):
|
|
456
|
+
|
|
457
|
+
| right | satisfied when | code |
|
|
458
|
+
| -- | -- | -- |
|
|
459
|
+
| access (Art. 15) | a `subject`-scoped touchpoint `read`s it | `access-missing` |
|
|
460
|
+
| rectification (Art. 16) | only for values the person provided: a `subject` `update`, or delete + create (re-creation) | `rectification-missing` |
|
|
461
|
+
| erasure (Art. 17) | a `subject` `delete`; `mode: anonymise` needs a ground to keep the row; `legal_obligation` activities exempt by construction | `erasure-missing` |
|
|
462
|
+
| storage limitation (Art. 5(1)(e)) | a `retention_purge` covering all rows, or purge cases + a delete path for the rest; a staff/system `delete` also ends the row's life | `retention-missing` |
|
|
463
|
+
| portability (Art. 20) | consent/contract, values the person provided, access served: a `portability` op or a JSON API the person calls on their own data | `portability-missing` |
|
|
464
|
+
| objection (Art. 21) | legitimate-interests activities: a `subject` update/delete on one of its items (an opt-out) | `objection-missing` |
|
|
465
|
+
| consent (Art. 7) | consent activities: `consent.record` created with `consent_for`, and a `consent_withdraw: {for: slug}` op | `consent-proof-missing`, `consent-withdrawal-missing` |
|
|
466
|
+
| transfers (Ch. V) | party outside the EEA / adequacy list (`knowledge/adequacy.yaml`) carries `safeguard: sccs|bcr|dpf|derogation` (`dpf` with `dpf_certified: true`); an unknown country is a Todo | `transfer-safeguard-missing` |
|
|
467
|
+
| DPIA (Art. 35) | special-category data (`always`) → `dpia_reference` on the activity; confidential data (`large_scale`) only when `app.yaml` says `large_scale: true` | `dpia-missing` |
|
|
468
|
+
|
|
469
|
+
When a staff screen performs the op but no self-service does, the finding
|
|
470
|
+
says so (*no self-service; staff can via admin:people.User — exempt
|
|
471
|
+
staff_only if a request process exists*). When every activity holding an
|
|
472
|
+
item is about `staff`/`employees`, the back-office is the person's own
|
|
473
|
+
interface and staff ops count as the subject's. Transient manual items
|
|
474
|
+
(`transient: true`) have no storage-side rights, only transfers. Library
|
|
475
|
+
models ship their own rights story (`knowledge/library/*.yaml` `rights:`
|
|
476
|
+
block: an audit trail is kept for accountability, a session is purged by the
|
|
477
|
+
framework) which applies to inherited columns too (a page type's `owner`).
|
|
478
|
+
|
|
479
|
+
Exemptions live on the **data item** (`<unit>/compliance/data/<id>.yaml`, or
|
|
480
|
+
`<app.Model>.*.yaml` for every personal field of a model; the item's own
|
|
481
|
+
file wins right by right):
|
|
482
|
+
|
|
483
|
+
```yaml
|
|
484
|
+
rights:
|
|
485
|
+
erase: {exempt: legal_obligation, note: "accounting records, 10 years"}
|
|
486
|
+
portability: {exempt: derived}
|
|
487
|
+
rectify: {exempt: staff_only} # verified: an admin op by staff must exist
|
|
488
|
+
access: {exempt: manual, note: "..."} # always listed under Review
|
|
489
|
+
retention: !missing "no purge task, see FAH-210"
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Grounds: `legal_obligation`, `contract_active` (still needs an event-driven
|
|
493
|
+
`erase`), `not_provided_by_subject` (portability), `derived`
|
|
494
|
+
(rectify/portability), `staff_only`, `manual`, `public_interest`, `research`,
|
|
495
|
+
`legal_claims`. Precedence for a right: item exemption → derived from ops →
|
|
496
|
+
missing. Every unmet right lands in the **Missing** section tagged with its
|
|
497
|
+
origin — `[derived]` (the tool), `[claimed]` (an agent that read the code,
|
|
498
|
+
via `data_flag` or a `{"missing": ...}` verdict in `activity_create`; the
|
|
499
|
+
note is prefixed `[agent]`), `[declared]` (a human's `!missing`) — and
|
|
500
|
+
`data why` prints each right's status next to the item's lifecycle.
|
|
501
|
+
|
|
502
|
+
### Threats
|
|
503
|
+
|
|
504
|
+
The threat model is a projection of the folder, not a new declaration.
|
|
505
|
+
Every touchpoint is a *process* (one node each), every store a *store*,
|
|
506
|
+
the actors (the person, staff, anyone, the system) and the parties with
|
|
507
|
+
transfers are *parties*, and *flows* join them: actor → touchpoint,
|
|
508
|
+
touchpoint → store (its ops), touchpoint → party (transfers), front route →
|
|
509
|
+
api operation (`calls`), touchpoint → task (`defers`). A flow carries the
|
|
510
|
+
declared items and their sensitivity.
|
|
511
|
+
|
|
512
|
+
The catalogue is pytm's threat library (`knowledge/threats/<SID>.yaml`,
|
|
513
|
+
generated by `threats gen` — a developer command that refuses a pytm
|
|
514
|
+
threat `_mapping.yaml` does not classify). The mapping says how each
|
|
515
|
+
threat is treated: `never` (impossible in our stacks — memory-safe
|
|
516
|
+
runtimes, no PHP/LDAP/SOAP — or infra we do not model — TLS, HTTP
|
|
517
|
+
smuggling, hosting), or a list of **dismissal rules** (`_rules.yaml`) any
|
|
518
|
+
of which closes the cell for an element. Rules are SIMPLE facts: the
|
|
519
|
+
touchpoint has no request (a task, a bare GET), returns JSON not HTML, has
|
|
520
|
+
no `mark_safe`/`{@html}` in its files, no `.raw(`, no `subprocess`, no XML
|
|
521
|
+
parser, no file input, is not cookie-authenticated (CSRF), is public by
|
|
522
|
+
design (ownership does not apply), the flow carries no personal item, no
|
|
523
|
+
credentials. No AST, no reasoning: when a rule cannot decide, the cell is
|
|
524
|
+
**open** and belongs to an agent under the threat's topic (access, auth,
|
|
525
|
+
input, disclosure, dos, files, xss, csrf, credentials, store).
|
|
526
|
+
|
|
527
|
+
```
|
|
528
|
+
uv run model-wtf compliance threats matrix # counts per kind and topic
|
|
529
|
+
uv run model-wtf compliance threats matrix --open # every open cell
|
|
530
|
+
uv run model-wtf compliance threats why api:getOrder # each threat: in/out and the rule
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
`check` folds the open cells into one Review line per unit (items
|
|
534
|
+
`unit:id#SID` ride on it for the gate). Undeclared touchpoints do not
|
|
535
|
+
count yet: their flows are unknown until they are reviewed.
|
|
536
|
+
|
|
537
|
+
#### Stamps
|
|
538
|
+
|
|
539
|
+
An open cell is closed by a **stamp** in the element's own YAML
|
|
540
|
+
(touchpoint manifest, `stores/<slug>.yaml`, `parties/<id>.yaml`):
|
|
541
|
+
|
|
542
|
+
```yaml
|
|
543
|
+
threats:
|
|
544
|
+
AC01: {status: mitigated, note: "get_object_or_404(user=request.user) api.py:245"}
|
|
545
|
+
DO02: {status: accepted, note: "list capped at 50 by CursorPagination"}
|
|
546
|
+
HA01: {status: n/a, note: "photo id is a UUID looked up in the DB; no path built"}
|
|
547
|
+
DS06: !missing "returns payment_method to anonymous callers (auth=None)"
|
|
548
|
+
DS06@party:mapbox: {status: mitigated, note: "only the position is sent"}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
`mitigated` (the code handles it; cite where), `accepted` (the risk owner
|
|
552
|
+
accepts; say why), `n/a` (the rule could not tell but the threat does not
|
|
553
|
+
apply here). A key is a SID — the element and every flow it is part of —
|
|
554
|
+
or `SID@<other end>` for one flow. `!missing` is a finding: **Missing**
|
|
555
|
+
(`threat-missing`, origin claimed/declared like rights). Stamps written by
|
|
556
|
+
the tool carry the element's fingerprint; when the code moves the stamp is
|
|
557
|
+
**stale** and the cell reopens. Re-declaring a touchpoint keeps its
|
|
558
|
+
stamps; the challenger's `reviews` tool lists them as assertions to
|
|
559
|
+
re-check.
|
|
560
|
+
|
|
561
|
+
```
|
|
562
|
+
uv run model-wtf compliance threats stamp api:getOrder AC01 --status mitigated --note "orders/api.py:283 scoped to request.user"
|
|
563
|
+
uv run model-wtf compliance threats stamp api:getOrder DS06 --missing "payment_method returned to anonymous callers"
|
|
564
|
+
uv run model-wtf compliance threats stamp api:checkout->api:db-default DS06 --status n/a --note "the app's own database"
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### Severity
|
|
568
|
+
|
|
569
|
+
A `!missing` stamp is weighed by the tool, not the agent: **impact ×
|
|
570
|
+
likelihood**. Impact is the *effect* — `disclosure`, `tampering`,
|
|
571
|
+
`destruction`, `denial`, `escalation`, `repudiation` (`_mapping.yaml`
|
|
572
|
+
carries one per threat; `ops` resolves from what the touchpoint does) — at
|
|
573
|
+
a *degree* (`existence` 0.25 < `attribute` 0.5 < `record` 1 < `bulk` 1.5,
|
|
574
|
+
inferred: lists, exports, admin screens, tasks and integer ids are `bulk`)
|
|
575
|
+
on the most sensitive item reached (`public` 0 … `special` 4); escalation
|
|
576
|
+
counts 4, denial is capped at 2. Likelihood is the most feared *actor* the
|
|
577
|
+
touchpoint's scope lets in — `anonymous`, `subject`, `staff`, `system`,
|
|
578
|
+
each with a `malice` and a `reach` in `knowledge/threats/_actors.yaml`,
|
|
579
|
+
overridable in `compliance/actors.yaml` — minus the actors already
|
|
580
|
+
**entitled** to that data through another declared touchpoint (staff
|
|
581
|
+
counting pictures they see in the back-office is not a finding). Buckets:
|
|
582
|
+
`critical` / `high` / `medium` / `low` / `info`. The reviewer may only
|
|
583
|
+
narrow (`--degree existence`, `--effect denial`, `--actor subject`) with a
|
|
584
|
+
reason; `check` tags and sorts findings by risk.
|
|
585
|
+
|
|
586
|
+
Every finding gets a stable id, `F-0042`, allocated in
|
|
587
|
+
`compliance/findings.lock.yaml` on first sighting and never reused (a fixed
|
|
588
|
+
finding is closed with a date, not deleted, so a ticket citing it still
|
|
589
|
+
resolves). `threats findings` lists them most severe first — several
|
|
590
|
+
threats with the same evidence on one element fold into one row — and
|
|
591
|
+
`threats why F-0042` explains one: weight, who, data, evidence, the
|
|
592
|
+
threat's description and mitigations. A declared, safeguarded transfer to
|
|
593
|
+
a party is the intended use, not a leak: disclosure threats on that flow
|
|
594
|
+
are dismissed by rule.
|
|
595
|
+
|
|
596
|
+
#### The swarm
|
|
597
|
+
|
|
598
|
+
```
|
|
599
|
+
uv run model-wtf compliance threats auto-review --unit api # one reviewer per topic
|
|
600
|
+
uv run model-wtf compliance threats auto-review --by touchpoint # one reviewer per touchpoint
|
|
601
|
+
uv run model-wtf compliance threats auto-review --elements api:getOrder,api:checkout
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
`auto-review` sends agents to stamp the open cells. By default one
|
|
605
|
+
reviewer per **topic** (access, auth, input, disclosure, dos, files, xss,
|
|
606
|
+
csrf, credentials, store, llm; `knowledge/threats/_topics.yaml` carries
|
|
607
|
+
each checklist) over a batch of touchpoints (`--topic-batch`, 12): the
|
|
608
|
+
same question asked of each touchpoint, answered from its code with
|
|
609
|
+
`threat_stamp`. `--by touchpoint` sends one reviewer per touchpoint with
|
|
610
|
+
all its open SIDs instead. Measured on Food@Home (14 subject-facing
|
|
611
|
+
endpoints, same commit): per topic closed every cell for 49k tokens and
|
|
612
|
+
found the unscoped `Cart`/`Order` lookups; per touchpoint spent 109k
|
|
613
|
+
tokens, stalled on 12 of 14 and missed them — small models do better with
|
|
614
|
+
one narrow question than with fifteen. The reviewer never reclassifies
|
|
615
|
+
data or edits ops; a stamp it cannot justify with a file:line stays open.
|
|
616
|
+
|
|
617
|
+
Introspection payloads are cached under `.git/model-wtf/introspect/`,
|
|
618
|
+
keyed by the source tree (paths, sizes, mtimes): a swarm of MCP servers
|
|
619
|
+
boots Django once, not once per tool call. `MODEL_WTF_NO_CACHE=1` bypasses.
|
|
620
|
+
|
|
621
|
+
### Use it in CI: the gate
|
|
622
|
+
|
|
623
|
+
Nobody expects a repository to be clean on day one; the gate expects it to
|
|
624
|
+
**not get worse**. `model-wtf compliance ghate` runs the whole `check`
|
|
625
|
+
twice — on the base ref, checked out into a temporary `git worktree` with
|
|
626
|
+
its own `compliance/` state, and on the head (the working tree by default,
|
|
627
|
+
so uncommitted work is gated too) — and fails only on findings the change
|
|
628
|
+
**introduces**.
|
|
629
|
+
|
|
630
|
+
```yaml
|
|
631
|
+
# .github/workflows/compliance.yml (written by `compliance init`)
|
|
632
|
+
name: compliance
|
|
633
|
+
on: [pull_request]
|
|
634
|
+
jobs:
|
|
635
|
+
gate:
|
|
636
|
+
runs-on: ubuntu-latest
|
|
637
|
+
steps:
|
|
638
|
+
- uses: actions/checkout@v4
|
|
639
|
+
with:
|
|
640
|
+
fetch-depth: 0
|
|
641
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
642
|
+
- uses: ModelW/wtf@v1
|
|
643
|
+
with:
|
|
644
|
+
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }} # optional
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
The action (`action.yml` at the root of this repository, `v1` tag) installs
|
|
648
|
+
uv and model-wtf, runs `uv sync --frozen` / `pnpm install` in every folder
|
|
649
|
+
holding a lockfile so introspection works, then runs the gate. Inputs:
|
|
650
|
+
`merge-into` (default: the PR base from the event), `fail-on-existing`,
|
|
651
|
+
`python-version`, `install-python-deps`, `install-node-deps`. Outputs:
|
|
652
|
+
`introduced`, `fixed`, `pre-existing`. Under Actions it emits one
|
|
653
|
+
`::error`/`::warning` annotation per introduced finding on the head's
|
|
654
|
+
files, a `::notice` verdict, and a Markdown table (introduced / fixed /
|
|
655
|
+
pre-existing per check) in the step summary.
|
|
656
|
+
|
|
657
|
+
Locally:
|
|
658
|
+
|
|
659
|
+
```
|
|
660
|
+
uv run model-wtf compliance ghate --merge-into develop # gate the working tree
|
|
661
|
+
uv run model-wtf compliance ghate --merge-into develop --head feature/x
|
|
662
|
+
uv run model-wtf compliance ghate --merge-into develop --format json
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Findings are compared by **identity** — `(scope, code, subject)`, where the
|
|
666
|
+
subject is a data id, a touchpoint id, an activity slug or `file#field`,
|
|
667
|
+
never a line number or a message. Folded lines (`12 data item(s) pending`)
|
|
668
|
+
are compared item by item, so a PR that adds an unreviewed personal field
|
|
669
|
+
fails with exactly that item, while a PR touching an unrelated file when
|
|
670
|
+
300 items were already pending passes. Fixed findings are reported too.
|
|
671
|
+
|
|
672
|
+
The base worktree gets the head's `.venv` / `node_modules` linked in when
|
|
673
|
+
the unit's lockfile is byte identical on both sides; otherwise the base
|
|
674
|
+
run is approximate and the gate says so (a dependency change is a
|
|
675
|
+
legitimate reason for new findings). Exit codes: 0 nothing introduced
|
|
676
|
+
(pre-existing findings are listed, not failed), 1 findings introduced, 3
|
|
677
|
+
declaration errors in the head (always the PR's fault), 4 tool error;
|
|
678
|
+
`--fail-on-existing` also fails on pre-existing findings for repositories
|
|
679
|
+
that are already clean.
|
|
680
|
+
|
|
681
|
+
#### The challenger
|
|
682
|
+
|
|
683
|
+
Fingerprints catch shape changes on data items (a field's type or
|
|
684
|
+
nullability); they cannot catch a view that starts mailing an address to a new
|
|
685
|
+
provider, a purge task that gets disabled, or a column re-purposed with the
|
|
686
|
+
same type. That is a reading job, so with an API key the gate first runs
|
|
687
|
+
the **challenger**: an agent with the full git checkout, `git diff` and
|
|
688
|
+
`grep`, the `reviews` tool (what reviewers asserted about the changed
|
|
689
|
+
files: classifications with their reasons, declared ops, transfers,
|
|
690
|
+
exemption notes — every one citing code) and one write tool, `challenge`.
|
|
691
|
+
It does not reclassify; it re-opens, with grounds citing the hunk.
|
|
692
|
+
|
|
693
|
+
`reviews` lists three kinds of assertion: data classifications, touchpoint
|
|
694
|
+
declarations and **threat stamps** (`threat api:getOrder#AC01: mitigated —
|
|
695
|
+
orders/api.py:283 scoped to request.user`). `challenge` takes any of the
|
|
696
|
+
three refs. A hunk that changes what a touchpoint reads, writes, returns or
|
|
697
|
+
sends re-opens the declaration; a hunk that removes the control a stamp
|
|
698
|
+
cites (a queryset scope, an auth class, a throttle, a validator, a cookie
|
|
699
|
+
setting) re-opens that one stamp — `element#SID` — and the cell is stale
|
|
700
|
+
(`threats why` shows the grounds) until re-stamped; both when both. A
|
|
701
|
+
`!missing` is a finding, not a claim: it cannot be challenged. Stores are
|
|
702
|
+
listed when a settings file is in the diff, since their stamps cite
|
|
703
|
+
settings.
|
|
704
|
+
|
|
705
|
+
A challenge is recorded in `data.lock.yaml` (`challenge: {commit,
|
|
706
|
+
grounds}`), in the touchpoint manifest, or on the stamp itself, and makes
|
|
707
|
+
the item `pending:challenged` (the cell `stale`), which the gate counts as
|
|
708
|
+
introduced. The record is
|
|
709
|
+
what keeps the non-determinism out of the gate: an item is challenged at
|
|
710
|
+
most once per change, a re-review (confirming is fine) moves the challenge
|
|
711
|
+
to `answered:` and the same grounds are refused afterwards — a false
|
|
712
|
+
positive costs one review, then silence. In CI the challenger's commit is
|
|
713
|
+
pushed to the PR branch (`commit-challenges`, default on); the developer
|
|
714
|
+
sees exactly which reviews to redo and why. The agent's shell never sees
|
|
715
|
+
the CI environment: only the OpenCode whitelist, and our own MCP server
|
|
716
|
+
drops `GITHUB_TOKEN` and friends.
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
uv run model-wtf compliance challenge --merge-into develop [--commit]
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Exit codes
|
|
723
|
+
|
|
724
|
+
| Code | Meaning |
|
|
725
|
+
| ---- | -------------------------------------------------------------------- |
|
|
726
|
+
| 0 | Clean (or only `!todo` questions, with `--allow-todo`) |
|
|
727
|
+
| 1 | Missing (`!missing`, never ignorable), Todo (`!todo`), Review (pending data / touchpoints, orphans) |
|
|
728
|
+
| 2 | Stale attestation |
|
|
729
|
+
| 3 | Declaration errors (schema, missing files, dangling party ids) |
|
|
730
|
+
| 4 | Tool error |
|
|
731
|
+
|
|
732
|
+
## Development
|
|
733
|
+
|
|
734
|
+
```
|
|
735
|
+
uv sync
|
|
736
|
+
make clean # format + lint + typecheck
|
|
737
|
+
make test
|
|
738
|
+
make docs # generate the reference pages and build the site into site/
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Releasing
|
|
742
|
+
|
|
743
|
+
The version is the git tag; `pyproject.toml` carries a `0.0.0` placeholder
|
|
744
|
+
that the release workflow replaces before building. To cut a release:
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
git tag v1.2.3 && git push origin v1.2.3 # a release
|
|
748
|
+
git tag v1.2.3rc1 && git push origin v1.2.3rc1 # a release candidate
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
`release.yml` builds, publishes to PyPI through trusted publishing (the
|
|
752
|
+
PyPI project trusts this repository's `release.yml` in the `pypi`
|
|
753
|
+
environment; no token is stored), moves the `v1` branch and tag so
|
|
754
|
+
`uses: ModelW/wtf@v1` follows the latest 1.x, and creates the GitHub
|
|
755
|
+
release with the matching `CHANGELOG.md` section. Write that section
|
|
756
|
+
before tagging. A candidate (`rc`, `a`, `b`) is published and released as a
|
|
757
|
+
pre-release under the final version's changelog section but does **not**
|
|
758
|
+
move `v1`: try it with `uses: ModelW/wtf@v1.2.3rc1` or `pip install
|
|
759
|
+
model-wtf==1.2.3rc1`.
|