Repository navigation
Expand file tree
/
Copy paththreat-model.yaml
More file actions
1819 lines (1798 loc) · 148 KB
/
Copy paththreat-model.yaml
File metadata and controls
1819 lines (1798 loc) · 148 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# threat-model.yaml — derived index; the prose threat model (threat-model.md) is canonical.
# Authority order: prose > yaml > json. Regenerate whenever the prose changes.
schema: threat-model-sidecar/v2
project: lens
prose_version: "threat-model.md@sha256:9a9d789465ffd6df5e5536f945c7ab2deabe634e68d8289ea9e73de4d6b23073"
model_status: under-review
triage_policy: strict
confidence: {documented: 201, maintainer: 46, inferred: 91, assumption: 8}
generation:
model: "Claude Opus 5.5 (claude-opus-5-5)"
effort: default
plugins:
- threat-model
- threat-model-recon
- threat-model-surface
- threat-model-interview
- threat-model-authoring
- threat-model-backtest
- threat-model-sidecar
x-roles:
orchestrator: "Claude Opus 5.5 (claude-opus-5-5)"
authoring: "Claude Opus 5.5 (claude-opus-5-5)"
revision: "Claude Opus 5.5 (claude-opus-5-5)"
backtest: "Claude Sonnet 5.5 (claude-sonnet-5-5)"
recon: "Claude Sonnet 5.5 (claude-sonnet-5-5)"
surface: "Claude Sonnet 5.5 (claude-sonnet-5-5)"
sidecar: "Claude Opus 5.5 (claude-opus-5-5)"
x-plugin-version: "threat-model 1.2.1"
x-modeled-commit: 5f799f2ff8bb8a762d8151d9b10b535a17878b55
x-model-date: "2026-10-05"
x-repository: "https://github.com/adeelahmad/lense"
# ---------------------------------------------------------------- §1.2 / §1.3 / §1.4
components:
- name: web
scope: in
x-family: "Web app & API gateway (Web)"
reachability_precondition: "reachable from a browser request and changes what the API decides or what another person's browser runs"
provenance: {kind: inferred, question_id: Q34}
- name: identity
scope: in
x-family: "Identity & sessions (Identity)"
reachability_precondition: "reachable with no account, or with an account, and yields a session, token or admin the attacker was not given; setup-code findings apply only while no account exists"
provenance: {kind: documented, source: 'docs/authentication.md "one-time **setup code**"'}
- name: authz
scope: in
x-family: "Authorization & data API (AuthZ)"
reachability_precondition: "lets a principal read or change more than its role and the asset's access level allow, or open media without a link the server signed for it"
provenance: {kind: documented, source: 'SECURITY.md "What counts"'}
- name: outbound
scope: in
x-family: "Outbound fetchers & stored secrets (Outbound)"
reachability_precondition: "a URL a non-admin sets, or a guarded admin operand, reaches a non-public address; or a stored secret reaches anyone but the host the admin set"
provenance: {kind: maintainer, date: "2026-10"}
- name: ingest
scope: in
x-family: "Ingest & processing (Ingest)"
reachability_precondition: "reachable from bytes an editor uploads or a watched source delivers, and the fault is in how Lens runs the tool (arguments, environment, timeouts, sandbox flags)"
provenance: {kind: inferred, question_id: Q34}
- name: query
scope: in
x-family: "Query engines (Query)"
reachability_precondition: "query text from a non-admin reads outside the caller's namespaces, writes, fetches, or reaches SurrealQL"
provenance: {kind: inferred, question_id: Q34}
- name: assistant
scope: in
x-family: "Assistant & extensions (Assistant)"
reachability_precondition: "a non-admin (or content steering the model for one) exceeds the person's reads, changes data without approval outside notes and admin setup, or runs code on the host"
provenance: {kind: inferred, question_id: Q34}
- name: optional
scope: in
x-family: "Optional services, off by default (Optional)"
reachability_precondition: "the service is turned on by an admin, and the attacker lacks the login, network or token the feature requires"
provenance: {kind: inferred, question_id: Q34}
- name: ci-tooling-and-samples
scope: out
reason: "examples/ (source clone only), overrides/ (mkdocs theme), ci/, .github/, .pre-commit* (CI, in no image) and prod-frontend-deploy.yml (stale Vercel workflow) are unsupported paths built into no running service (§1.3)"
provenance: {kind: assumption, question_id: Q34}
- name: inert-backend-files
scope: out
reason: "fastapi_backend/tests/, watcher.py and commands/ are copied into the backend image but never started by the production command; a report must show production code reaching them (§1.3)"
provenance: {kind: assumption, question_id: Q34}
# ---------------------------------------------------------------- §1.5
host_side_effects:
- effect: network
stance: conditional
components: [identity, outbound, assistant, optional]
conditions: "outbound connections only to configured sources, model servers, mail, notification and telemetry targets; telemetry off by default; no update check; components.auto (on by default) runs pip installs and fetches hashed model files; llama.cpp and cloudflared from GitHub 'latest' when those features are on; hf: models from Hugging Face"
provenance: {kind: maintainer, date: "2026-10"}
- effect: x-other-outbound-calls
stance: absent
components: [web, identity, authz, outbound, ingest, query, assistant, optional]
conditions: "none found in Lens code outside the configured destinations"
provenance: {kind: assumption, question_id: Q19}
- effect: x-ml-extra-weight-downloads
stance: conditional
components: [ingest]
conditions: "some optional ML extras download weights on first use on their own (Hugging Face, ModelScope)"
provenance: {kind: inferred, question_id: Q19}
- effect: environment-reads
stance: conditional
components: [outbound]
conditions: "plain urllib callers honour HTTP(S)_PROXY; the guarded openers ignore it"
provenance: {kind: assumption, question_id: Q19}
- effect: child-processes
stance: present
components: [ingest, outbound, assistant, optional]
conditions: "rclone, ffmpeg/ffprobe, LibreOffice, Chromium, Tesseract, poppler tools, antiword/catdoc, cloudflared, llama-server, unshare + Python for code tools, uv/pip; all as argument lists"
provenance: {kind: assumption, question_id: Q20}
- effect: network-sockets
stance: conditional
components: [web, outbound, optional]
conditions: "API and web app listen; a netguard proxy on loopback during captures; MQTT 1883 and syslog 5514 on sensors.bind (default 0.0.0.0) when sensors are on; llama-server on its port (0.0.0.0 in containers, with its own API key)"
provenance: {kind: assumption, question_id: Q19}
- effect: x-log-credentials
stance: present
components: [identity]
conditions: "the setup code and a /setup?code= link at WARNING; sign-in and reset links when mail is not set"
provenance: {kind: documented, source: 'docs/authentication.md "the API prints in its log"'}
- effect: filesystem
stance: present
components: [authz, outbound, ingest]
conditions: "everything under data_dir, including secret.key (mode 600) when ARCHIVE_SECRET_KEY is unset; temp dirs for imports, rclone config, LibreOffice and Chromium profiles"
provenance: {kind: inferred, question_id: Q19}
- effect: global-state
stance: present
components: [query, optional]
conditions: "rdflib SPARQL_LOAD_GRAPHS = False set on each SPARQL call; sys.path gains data_dir/python (components); a signal handler in the CLI"
provenance: {kind: assumption, question_id: Q19}
- effect: x-secrets-to-children
stance: conditional
components: [optional]
conditions: "tunnel token passed to cloudflared in its environment, not its command line (promoted to P-CHILD-SECRETS)"
provenance: {kind: documented, source: 'docs/remote-access.md "in its environment, not on its command line"'}
# ---------------------------------------------------------------- §1.7 trust table
entry_points:
- id: "every /api/v1/* route"
component: identity
parameters:
- name: "Authorization: Bearer (web JWT, la_ API token, lo_ app token)"
attacker_controllable: true
control_kinds: [data, serialized-state]
obligation_id: keep-bearer-secret
caller_must_enforce: "holder keeps it secret; Lens checks signature or hash, expiry, account, session"
provenance: {kind: documented, source: 'docs/authentication.md "Every request checks that the account is still active"'}
- id: "every request"
component: web
parameters:
- name: "Host header"
attacker_controllable: true
control_kinds: [resource-name]
obligation_id: list-served-hosts
caller_must_enforce: "operator lists served hosts"
provenance: {kind: documented, source: 'docs/configuration.md "checks the `Host` header"'}
- id: "IP groups, throttles"
component: authz
parameters:
- name: "X-Forwarded-For, peer address"
attacker_controllable: true
x-condition: "honoured only from trusted proxies"
control_kinds: [x-trusted-header]
obligation_id: trusted-proxy-chain
caller_must_enforce: "a reverse proxy sets the header; trusted_proxies lists exactly the front hops"
provenance: {kind: documented, source: 'docs/configuration.md "Without it, a visitor can claim any address"'}
- id: "passkey origin, OAuth discovery, external-login redirect"
component: identity
parameters:
- name: "X-Forwarded-Host, X-Forwarded-Proto"
attacker_controllable: true
x-condition: "honoured for served hosts or from trusted proxies"
control_kinds: [resource-name]
obligation_id: trust-proxy-headers-behind-proxy
caller_must_enforce: "TRUST_PROXY_HEADERS only behind a real proxy"
provenance: {kind: documented, source: 'docs/authentication.md "anyone could choose the address"'}
- id: "POST /auth/login, /iiif/auth/access form"
component: identity
parameters:
- name: "email, password"
attacker_controllable: true
control_kinds: [data, size, rate]
caller_must_enforce: "none — P-PASSWORDS-OFF (passwords work only where auth.passwords is on)"
provenance: {kind: documented, source: 'docs/authentication.md "With passwords off, password sign-in"'}
- id: "/auth/setup, /auth/passkey/setup*, /auth/setup/no-passkey"
component: identity
parameters:
- name: "setup code, email, name"
attacker_controllable: true
control_kinds: [data]
obligation_id: private-log-until-first-admin
caller_must_enforce: "keep the log private until the first admin exists"
provenance: {kind: documented, source: 'docs/configuration.md "so a stranger who finds a new"'}
- id: "/auth/refresh, /auth/ticket, /auth/signin-link/*, /auth/password/*, WebAuthn flows"
component: identity
parameters:
- name: "rotating and one-time secrets"
attacker_controllable: true
control_kinds: [data]
obligation_id: keep-one-time-secrets
caller_must_enforce: "holder keeps them secret"
provenance: {kind: documented, source: 'docs/authentication.md "A link lasts three days"'}
- id: "/oauth/register, /authorize, /token, /revoke"
component: identity
parameters:
- name: "redirect URIs, client metadata, code, verifier, refresh token"
attacker_controllable: true
x-condition: "registration is anonymous; authorize runs in the victim's browser"
control_kinds: [data, resource-name]
obligation_id: oauth-client-state-and-consent
caller_must_enforce: "the app keeps state and its verifier; the person consents"
provenance: {kind: documented, source: 'docs/authentication.md "Apps register themselves"'}
- id: "/auth/external/{key}/*"
component: identity
parameters:
- name: "next, state, code, provider profile"
attacker_controllable: true
x-condition: "profile only as far as the provider allows"
control_kinds: [data, x-collaborator-data]
obligation_id: admin-chooses-providers
caller_must_enforce: "admin chooses providers"
provenance: {kind: documented, source: 'docs/authentication.md "when the provider vouches for the email"'}
- id: "/mcp"
component: identity
parameters:
- name: "bearer, Origin, JSON-RPC body"
attacker_controllable: true
control_kinds: [data, size, rate]
caller_must_enforce: "none — P-OAUTH"
provenance: {kind: documented, source: 'docs/mcp.md "A token an app asked for another server"'}
- id: "namespace, recording and child-object routes"
component: authz
parameters:
- name: "namespace name, rid, child ids (sequential integers)"
attacker_controllable: true
control_kinds: [resource-name]
caller_must_enforce: "none — P-NS-ISOLATION (Lens checks the role in each handler)"
provenance: {kind: documented, source: 'docs/api.md "Namespaces you can''t read answer 404"'}
- id: "routes not read one by one (about 370)"
component: authz
parameters:
- name: "per route"
attacker_controllable: true
x-condition: "per gate"
control_kinds: [x-per-route]
obligation_id: follow-gate-pattern
caller_must_enforce: "follow the gate pattern"
provenance: {kind: inferred, question_id: Q8}
- id: "media, report, embed, IIIF content"
component: authz
parameters:
- name: "exp, sig, full, ?s= share token, /s/<code> short code"
attacker_controllable: true
control_kinds: [serialized-state]
obligation_id: keep-link-private
caller_must_enforce: "holder keeps the link private"
provenance: {kind: documented, source: 'SECURITY.md "only the server''s own signed links should work"'}
- id: "/iiif/auth/token, /probe, /logout"
component: authz
parameters:
- name: "origin, messageId, IIIF cookie, probe token"
attacker_controllable: true
x-condition: "any site can frame the token page"
control_kinds: [resource-name]
obligation_id: set-iiif-allowed-origins
caller_must_enforce: "operator sets iiif.allowed_origins"
provenance: {kind: documented, source: 'docs/iiif.md "limits which viewer sites can get tokens"'}
- id: "/recordings, /search, /public/search, chat retrieval"
component: query
parameters:
- name: "query text, filters, sort, limit, offset"
attacker_controllable: true
x-condition: "/public/search is anonymous"
control_kinds: [data, size]
caller_must_enforce: "none — P-NO-INJECTION"
provenance: {kind: documented, source: 'SECURITY.md "injection into the database queries"'}
- id: "/graph/query, MCP graph_query"
component: query
parameters:
- name: "Cypher text, params"
attacker_controllable: true
x-condition: "any signed-in person"
control_kinds: [callback-code]
caller_must_enforce: "none — P-QUERY"
provenance: {kind: documented, source: 'docs/graph.md "3 million steps"'}
- id: "/namespaces/{name}/sparql"
component: query
parameters:
- name: "SPARQL text"
attacker_controllable: true
x-condition: "namespace readers"
control_kinds: [callback-code]
caller_must_enforce: "none — P-NO-FETCH"
provenance: {kind: documented, source: 'routes/rdf.py sparql docstring "SERVICE and FROM aren''t allowed"'}
- id: "/namespaces/{name}/rdf/import"
component: query
parameters:
- name: "Turtle, N-Triples, JSON-LD"
attacker_controllable: true
x-condition: "editors"
control_kinds: [serialized-state, size]
caller_must_enforce: "none — P-NO-FETCH"
provenance: {kind: documented, source: 'rdf_import.py docstring "RDF/XML isn''t read"'}
- id: "/uploads, /import, /recordings/{rid}/files"
component: ingest
parameters:
- name: "file bytes, name, extension, size"
attacker_controllable: true
x-condition: "editors"
control_kinds: [data, type-class, size]
caller_must_enforce: "none — P-UPLOAD"
provenance: {kind: documented, source: 'docs/configuration.md "Uploads accept only the types"'}
- id: "watched folders, storage sources, IMAP, iCal"
component: outbound
parameters:
- name: "remote file names, bytes, mail, events"
attacker_controllable: true
x-condition: "whoever writes to the remote, mails the box or serves the feed"
control_kinds: [data, resource-name, size]
obligation_id: admin-creates-sources
caller_must_enforce: "admin creates sources"
provenance: {kind: inferred, question_id: Q17}
- id: "Google Calendar sources"
component: outbound
parameters:
- name: "service account key file or connected account's refresh token, calendar ids, impersonate; the events Google returns"
attacker_controllable: true
x-condition: "key and token: admins only, sealed and write-only; events: anyone who can invite the account to an event or edit a calendar it reads"
control_kinds: [data, x-credential, x-prompt-injection, size]
obligation_id: admin-creates-sources
caller_must_enforce: "admin creates sources; read-only calendar.readonly scope; Google's own token endpoint and API address (a key file's token_uri is ignored); event text escaped into its .ics without control characters"
provenance: {kind: documented, source: 'docs/processing.md "The token endpoint is always Google''s own"'}
- id: "POST /imports/web, extension http tools, notification targets"
component: outbound
parameters:
- name: "URL"
attacker_controllable: true
x-condition: "editors (web, tools), owners (targets)"
control_kinds: [resource-name, x-network-destination]
caller_must_enforce: "none — P-SSRF-GUARD"
provenance: {kind: maintainer, date: "2026-10"}
- id: "admin settings (model, embeddings, speech, decisions, Fedora, bridge, telemetry, IIIF import, rclone remotes, IMAP, iCal, MQTT bridge, OIDC issuer)"
component: outbound
parameters:
- name: "URL, credentials, api_key_env"
attacker_controllable: false
x-condition: "admin only; documented guards still apply (P-ADMIN-GUARD)"
control_kinds: [resource-name, x-credential]
obligation_id: admin-is-trusted
caller_must_enforce: "admin is trusted"
provenance: {kind: maintainer, date: "2026-10"}
- id: "replies from remote servers"
component: outbound
parameters:
- name: "web page HTML and scripts, IIIF manifests, model replies, OIDC discovery, calendars and their redirects"
attacker_controllable: true
control_kinds: [data, object-topology]
obligation_id: treat-remote-replies-untrusted
caller_must_enforce: "treat as untrusted"
provenance: {kind: inferred, question_id: Q17}
- id: "chat, /graph/ask, MCP tools"
component: assistant
parameters:
- name: "prompt text; model-chosen tool calls"
attacker_controllable: true
x-condition: "also indirectly through content"
control_kinds: [data, x-prompt-injection]
caller_must_enforce: "none — P-RETRIEVAL"
provenance: {kind: documented, source: 'docs/assistant.md "limited to what the person can read"'}
- id: "Lens extension side panel"
component: assistant
parameters:
- name: "answers shown there (model text that can repeat third-party text), the sign-in coming back"
attacker_controllable: true
x-condition: "indirectly through content"
control_kinds: [data, x-prompt-injection]
caller_must_enforce: "none; answers render as Markdown cleaned by DOMPurify (no scripts, images or forms; only http and https links), and the sign-in checks its state and PKCE"
provenance: {kind: documented, source: 'extensions/browser/src/core/assistant.js answerHtml docstring "cleaned of anything that could run or fetch on its own"'}
- id: "extensions (prompt, graph and http tools, hooks), custom nodes including template nodes"
component: assistant
parameters:
- name: "tool spec, graph JSON, template text"
attacker_controllable: true
x-condition: "any writer, for their own; sharing is gated; template-node reach is Q23"
control_kinds: [callback-code]
obligation_id: enable-only-trusted-extensions
caller_must_enforce: "person enables only extensions they trust"
provenance: {kind: documented, source: 'docs/assistant.md "other people''s run only when an admin shared them"'}
- id: "Python code tools, shipped and admin-created templates, workflows, pipelines, routines"
component: assistant
parameters:
- name: "Python, Jinja source, regexes"
attacker_controllable: false
x-condition: "admin only"
control_kinds: [callback-code]
obligation_id: admin-is-trusted
caller_must_enforce: "admin is trusted"
provenance: {kind: maintainer, date: "2026-10"}
- id: "MQTT 1883, syslog 5514, POST /sensors/push/{token}/{stream}"
component: optional
parameters:
- name: "payloads, hub logins, token in the path"
attacker_controllable: true
x-condition: "once enabled"
control_kinds: [data, size, rate, x-credential]
obligation_id: enable-and-firewall-sensors
caller_must_enforce: "admin enables; operator firewalls"
provenance: {kind: documented, source: 'docs/sensors.md "Sensors are off until an admin turns them on"'}
- id: "NFS share (port 12049 on the host, 2049 in the worker)"
component: optional
parameters:
- name: "NFS and MOUNT RPCs, client address"
attacker_controllable: true
x-condition: "once enabled, from nfs.allowed_networks"
control_kinds: [data, x-network-source]
obligation_id: enable-and-limit-nfs
caller_must_enforce: "admin enables and picks the account; operator firewalls"
provenance: {kind: documented, source: 'docs/file-shares.md "NFS has no login"'}
- id: "SMB share (port 1445 on the host, 4445 in the worker)"
component: optional
parameters:
- name: "SMB2 requests, NTLM sign-in with a file access key, client address"
attacker_controllable: true
x-condition: "once enabled, from smb.allowed_networks"
control_kinds: [data, x-credential, x-network-source]
obligation_id: enable-and-limit-smb
caller_must_enforce: "admin enables; operator firewalls"
provenance: {kind: documented, source: 'docs/smb.md "What keeps it safe"'}
- id: ".env, archive.yaml, LENS_*, ARCHIVE_SECRET_KEY, RCLONE_BINARY"
component: outbound
parameters:
- name: "process configuration"
attacker_controllable: false
x-condition: "operator"
control_kinds: [x-env-config]
obligation_id: protect-process-config
caller_must_enforce: "keep unreadable to others"
provenance: {kind: maintainer, date: "2026-10"}
- id: "workers and DB peers"
component: authz
parameters:
- name: "DB credential, shared database rows"
attacker_controllable: false
x-condition: "trusted peer"
control_kinds: [x-peer]
obligation_id: trusted-peer-hosts
caller_must_enforce: "run peers on trusted hosts"
provenance: {kind: inferred, question_id: Q9}
- id: "host control inbox and the monitoring scrape token"
component: authz
parameters:
- name: "inbox/upgrade request: release ref (empty: the version running) with https=0|1, public=0|1 and target=full|lean, or action=services with monitoring=on|off"
attacker_controllable: true
x-condition: "writable by the containers and local users"
control_kinds: [x-env-config]
obligation_id: agent-allow-list
caller_must_enforce: "the agent installs only a release tag or the install's branch, changes only the HTTPS ports, the web app's network reach and the edition (other values dropped), and only toggles the monitoring profile; a writer can at most restart Lens with those choices or open it to the network"
provenance: {kind: documented, source: "docs/updates.md"}
- name: "/metrics scrape token, .lens-control/prometheus-token"
attacker_controllable: true
x-condition: "anyone reaching the web app; the file is world-readable on the host"
control_kinds: [credential]
obligation_id: metrics-token
caller_must_enforce: "token compared in constant time; metrics carry no personal content"
provenance: {kind: documented, source: "docs/prometheus.md Privacy"}
- id: "admin machine setup requests"
component: authz
parameters:
- name: "PUT /api/v1/admin/install {public, target}, POST /api/v1/settings/https/ports {open}, MCP set_install_settings"
attacker_controllable: false
x-condition: "admin only (MCP: with the write scope)"
control_kinds: [x-env-config]
obligation_id: admin-is-trusted
caller_must_enforce: "admin is trusted; audited (lens.install, https.ports); reaches the machine only as an inbox request the agent checks again"
provenance: {kind: documented, source: 'docs/updates.md "Setting the machine up again"'}
- id: "entity and topic curation tools"
component: authz
parameters:
- name: "MCP edit_entity, edit_topic, merge_topics, delete_topic, undo_topic_change, undo_graph_change; assistant propose_topic_change: entity and topic ids, names, labels, definitions"
attacker_controllable: true
x-condition: "an agent with an editor's write-scope token; indirectly through content the model reads"
control_kinds: [data, x-prompt-injection]
caller_must_enforce: "none — the curate permission in each namespace touched and a write-capable principal (the web app's own route functions); the assistant's changes wait for the person's approval; merges and deletes are kept to undo, and audited"
provenance: {kind: documented, source: 'docs/mcp.md "curate entities and topics as an editor does"'}
- id: "microsandbox program requests"
component: authz
parameters:
- name: "ask/NAME.want in /data/.lens/programs: a program's name"
attacker_controllable: true
x-condition: "writ
10BC8
able by the Lens user inside the sandbox"
control_kinds: [x-env-config]
obligation_id: program-allow-list
caller_must_enforce: "the start script installs only libreoffice or chromium (fixed Debian package lists), answers in a root-only folder, reads only the name"
provenance: {kind: documented, source: "docs/microsandbox.md LibreOffice and Chromium"}
# ---------------------------------------------------------------- §1.7 contract-dimension matrix
# Every in-scope component × the eight required dimensions appears exactly once.
# Prose sub-rows that refine a standard dimension (for example "Numeric domain:
# absolute session cap") are carried as x- dimensions so each standard cell
# stays unique. `x-routes-to` keeps the prose "Routes to" cell when it names
# more than the single property_id.
contract_dimensions:
# --- Web
- {component: web, dimension: numeric-domain, status: not-applicable, conditions: "bodies pass through; size caps are the API's", x-routes-to: "§1.7 rows", provenance: {kind: documented, source: 'backend-proxy.ts docstring "Serves the FastAPI backend''s paths on this origin"'}}
- {component: web, dimension: failure-atomicity, status: not-applicable, conditions: "no server state; only the encrypted session cookie", x-routes-to: "P-AUTH-REQ", provenance: {kind: documented, source: 'docs/configuration.md "encrypts the NextAuth session cookie"'}}
- {component: web, dimension: recursive-cyclic-topology, status: not-applicable, conditions: "stateless proxy, no recursive input; forwards bytes", provenance: {kind: inferred, question_id: Q34}}
- {component: web, dimension: concurrency-reentrancy, status: not-applicable, conditions: "stateless proxy; no shared mutable state", provenance: {kind: inferred, question_id: Q34}}
- {component: web, dimension: callback-execution, status: unresolved, conditions: "the API authorizes every call; proxy.ts only redirects pages", x-routes-to: "N-WEBGATE", question_id: Q8, provenance: {kind: inferred, question_id: Q8}}
- {component: web, dimension: serialization-reconstruction, status: claimed, property_id: P-AUTH-REQ, conditions: "session cookie encrypted with AUTH_SECRET (dev secret voids it)", provenance: {kind: documented, source: 'docs/authentication.md "takes effect immediately"'}}
- {component: web, dimension: reference-lifecycle, status: claimed, property_id: P-AUTH-REQ, conditions: "sign-out ends the session at once", provenance: {kind: documented, source: 'docs/authentication.md "takes effect immediately"'}}
- {component: web, dimension: resource-complexity, status: disclaimed, property_id: N-VOLUME, conditions: "no rate limit beyond sign-in throttles", provenance: {kind: documented, source: 'SECURITY.md "denial of service by sheer volume"'}}
- {component: web, dimension: x-browser-rendering, status: unresolved, conditions: "lower-role text rendered in another person's browser", question_id: Q24, provenance: {kind: inferred, question_id: Q24}}
- {component: web, dimension: x-headers, status: claimed, property_id: P-HARDENED, conditions: "web app page headers and API framing as documented", x-routes-to: "P-HARDENED, P-FRAME; others N-HEADERS", provenance: {kind: documented, source: 'docs/deployment.md "What''s hardened already"'}}
- {component: web, dimension: x-forwarded-host, status: unresolved, conditions: "which header chooses the address OAuth discovery, the passkey origin and external-login redirects name", question_id: Q38, provenance: {kind: inferred, question_id: Q38}}
# --- Identity
- {component: identity, dimension: numeric-domain, status: claimed, property_id: P-LIFETIMES, conditions: "token and link lifetimes and tokens.max_days", provenance: {kind: documented, source: 'docs/authentication.md "A link lasts three days"'}}
- {component: identity, dimension: x-numeric-domain-absolute-session-cap, status: unresolved, conditions: "the 168 h window resets on every refresh", question_id: Q4, provenance: {kind: inferred, question_id: Q4}}
- {component: identity, dimension: failure-atomicity, status: claimed, property_id: P-ONCE, conditions: "one-time secrets spent before use; a refused attempt spends nothing; one code makes one first admin", x-routes-to: "P-ONCE, P-SETUP-ONCE", provenance: {kind: documented, source: 'docs/authentication.md "works once"'}}
- {component: identity, dimension: recursive-cyclic-topology, status: not-applicable, conditions: "flat credentials; no nested identity input", provenance: {kind: inferred, question_id: Q34}}
- {component: identity, dimension: callback-execution, status: disclaimed, property_id: N-OIDC, conditions: "identity from the provider's token and userinfo endpoints", x-routes-to: "N-OIDC, N-PROVIDER", provenance: {kind: documented, source: 'docs/authentication.md "token and userinfo endpoints"'}}
- {component: identity, dimension: serialization-reconstruction, status: claimed, property_id: P-AUTH-REQ, conditions: "one key signs access tokens and media links", x-routes-to: "P-AUTH-REQ, P-MEDIA-LINK", provenance: {kind: documented, source: 'docs/configuration.md "signs access tokens and media links"'}}
- {component: identity, dimension: reference-lifecycle, status: claimed, property_id: P-AUTH-REQ, conditions: "sign-out, disable, revoke: immediate for every token kind; revoking a grant or token ends its tokens", provenance: {kind: documented, source: 'docs/authentication.md "stop working at once"'}}
- {component: identity, dimension: x-reference-lifecycle-credential-change, status: unresolved, conditions: "password change, passkey removal: API tokens, app grants, IIIF cookies survive", question_id: Q3, provenance: {kind: inferred, question_id: Q3}}
- {component: identity, dimension: x-revoke-race, status: unresolved, conditions: "a token minted while its grant is being revoked", question_id: Q39, provenance: {kind: inferred, question_id: Q39}}
- {component: identity, dimension: x-link-supersession, status: unresolved, conditions: "whether anyone but the owner or an admin can cancel a pending sign-in link", question_id: Q41, provenance: {kind: inferred, question_id: Q41}}
- {component: identity, dimension: x-redirect-target, status: unresolved, conditions: "post-sign-in next target", question_id: Q40, provenance: {kind: inferred, question_id: Q40}}
- {component: identity, dimension: concurrency-reentrancy, status: claimed, property_id: P-REUSE, conditions: "web refresh: replay within 60 s accepted; after that it ends the whole session", provenance: {kind: documented, source: 'docs/authentication.md "it ends the whole session"'}}
- {component: identity, dimension: x-concurrency-oauth-refresh, status: claimed, property_id: P-REUSE, conditions: "OAuth refresh: replay within 60 s refused, changes nothing; after that it ends the access", provenance: {kind: documented, source: 'docs/authentication.md "A swapped one that comes back within 60 seconds is refused"'}}
- {component: identity, dimension: resource-complexity, status: claimed, property_id: P-THROTTLE, conditions: "failed password sign-ins: 8 per 15 minutes; other anonymous routes are Q5", provenance: {kind: documented, source: 'docs/authentication.md "throttled per email and address"'}}
- {component: identity, dimension: x-resource-complexity-setup-routes, status: unresolved, conditions: "setup-code guesses", question_id: Q42, provenance: {kind: inferred, question_id: Q42}}
- {component: identity, dimension: x-enumeration, status: unresolved, conditions: "passkey 404, external-login messages, anonymous 401 vs 404", question_id: Q6, provenance: {kind: inferred, question_id: Q6}}
- {component: identity, dimension: x-mcp-audience, status: unresolved, conditions: "/mcp accepts tokens that name no server; the docs intro says MCP only reads", question_id: Q13, provenance: {kind: inferred, question_id: Q13}}
- {component: identity, dimension: x-csrf, status: claimed, property_id: P-BEARER-ONLY, conditions: "bearer-only API; IIIF cookie flow and NextAuth handlers excepted", provenance: {kind: documented, source: 'docs/architecture.md "so there is no CSRF surface"'}}
- {component: identity, dimension: x-forwarded-host, status: unresolved, conditions: "which header chooses the address OAuth discovery, the passkey origin and external-login redirects name", question_id: Q38, provenance: {kind: inferred, question_id: Q38}}
# --- AuthZ
- {component: authz, dimension: numeric-domain, status: claimed, property_id: MX-AUTHZ-LIST-CAPS, conditions: "/recordings at most 1000 rows; other lists capped per route; REST offset uncapped (N-VOLUME)", x-routes-to: "§1.7", provenance: {kind: documented, source: 'docs/api.md "at most 1000"'}}
- {component: authz, dimension: x-numeric-domain-mcp-tool-arguments, status: unresolved, conditions: "MCP tool offsets and page sizes (for example get_entity's mentions offset) have no documented maximum", question_id: Q47, provenance: {kind: inferred, question_id: Q47}}
- {component: authz, dimension: failure-atomicity, status: unresolved, conditions: "delete commits rows then removes files; move, merge, undo are stepwise", question_id: Q12, provenance: {kind: inferred, question_id: Q12}}
- {component: authz, dimension: recursive-cyclic-topology, status: claimed, property_id: P-HIERARCHY, conditions: "collections at most 8 deep, no cycles, 5000 per namespace", provenance: {kind: documented, source: 'docs/api.md "at most 8 deep"'}}
- {component: authz, dimension: callback-execution, status: not-applicable, conditions: "no caller code runs; authorization reads DB rows only", provenance: {kind: inferred, question_id: Q8}}
- {component: authz, dimension: serialization-reconstruction, status: claimed, property_id: P-MEDIA-LINK, conditions: "signature binds path, expiry and full; other query parameters unsigned", provenance: {kind: documented, source: 'core/security.py sign_path docstring "Extra query parameters are kept but not signed"'}}
- {component: authz, dimension: reference-lifecycle, status: disclaimed, property_id: N-SIGNED-BEARER, conditions: "signed links valid to expiry; not revocable; not bound to a person", provenance: {kind: documented, source: 'core/security.py sign_path docstring "grants read access to exactly this path until it expires"'}}
- {component: authz, dimension: x-reference-lifecycle-share-report-links, status: unresolved, conditions: "share and report links after the creator loses a role; after a move", question_id: Q10, provenance: {kind: inferred, question_id: Q10}}
- {component: authz, dimension: concurrency-reentrancy, status: unresolved, conditions: "roles load per request; caches key on the readable set", question_id: Q8, provenance: {kind: inferred, question_id: Q8}}
- {component: authz, dimension: resource-complexity, status: disclaimed, property_id: N-VOLUME, conditions: "no rate limit on read APIs", provenance: {kind: documented, source: 'SECURITY.md "denial of service by sheer volume"'}}
- {component: authz, dimension: x-access-parts, status: claimed, property_id: P-ACCESS-PARTS, conditions: "access levels, open parts, description", x-routes-to: "P-ACCESS-PARTS, N-PUBLIC", provenance: {kind: documented, source: 'docs/access.md "the parts of a public asset anyone may use"'}}
- {component: authz, dimension: x-client-address-compose, status: claimed, property_id: P-IPGROUP, conditions: "both compose files trust the web app container, so each visitor gets their own address", x-routes-to: "P-IPGROUP, P-THROTTLE", provenance: {kind: documented, source: 'docs/deployment.md "so each visitor gets their own"'}}
- {component: authz, dimension: x-client-address-other-variants, status: unresolved, conditions: "web app alone, installers, NAS, Proxmox without a reverse proxy setting X-Forwarded-For", question_id: Q2, provenance: {kind: inferred, question_id: Q2}}
- {component: authz, dimension: x-share-scope, status: unresolved, conditions: "?s= share tokens open the audio, video, frames and page API routes", question_id: Q7, provenance: {kind: inferred, question_id: Q7}}
# --- Outbound
- {component: outbound, dimension: numeric-domain, status: claimed, property_id: P-SSRF-GUARD, conditions: "web paths and iCal: ports 80 and 443", x-routes-to: "P-SSRF-GUARD, P-ADMIN-GUARD", provenance: {kind: documented, source: 'docs/api.md "Only public addresses are reached, on ports 80 and 443"'}}
- {component: outbound, dimension: x-numeric-domain-notification-ports, status: unresolved, conditions: "notification targets may use any port; the guard checks addresses only", question_id: Q15, provenance: {kind: inferred, question_id: Q15}}
- {component: outbound, dimension: failure-atomicity, status: claimed, property_id: P-NOTIFY, conditions: "each notification event claimed once across processes, then retried", provenance: {kind: documented, source: 'docs/notifications.md "claimed once"'}}
- {component: outbound, dimension: recursive-cyclic-topology, status: claimed, property_id: P-SSRF-GUARD, conditions: "redirects: http and https only, each hop re-checked; credentials never follow a redirect to another server; notifications follow none", x-routes-to: "P-SSRF-GUARD, P-SECRETS", provenance: {kind: documented, source: 'docs/processing.md "never sent on to another server"'}}
- {component: outbound, dimension: callback-execution, status: disclaimed, property_id: N-CHROMIUM, conditions: "Chromium may lack its sandbox; the proxy is the barrier", provenance: {kind: documented, source: 'docs/deployment.md "what stops a page is Lens''s proxy"'}}
- {component: outbound, dimension: serialization-reconstruction, status: claimed, property_id: P-SECRETS, conditions: "sealed secrets are write-only through the API", provenance: {kind: documented, source: 'docs/configuration.md "the API reports whether one is set, never its value"'}}
- {component: outbound, dimension: x-admin-operand-guards, status: claimed, property_id: P-ADMIN-GUARD, conditions: "iCal public-address guard; telemetry endpoint without user:password or link-local address", provenance: {kind: documented, source: 'docs/processing.md "fetched the way web pages are captured"'}}
- {component: outbound, dimension: x-admin-operand-guards-telemetry-headers, status: unresolved, conditions: "saved headers dropped when the endpoint moves to another host", question_id: Q35, provenance: {kind: inferred, question_id: Q35}}
- {component: outbound, dimension: x-remote-identity-imap, status: claimed, property_id: P-REMOTE-NAMES, conditions: "a rebuilt mailbox gets new names, so an old name never reads another message", provenance: {kind: documented, source: 'docs/processing.md "an old one never reads another message"'}}
- {component: outbound, dimension: x-remote-identity-other-sources, status: unresolved, conditions: "rclone, iCal and IIIF cache keys and names", question_id: Q43, provenance: {kind: inferred, question_id: Q43}}
- {component: outbound, dimension: reference-lifecycle, status: unresolved, conditions: "key placement: secret.key in data_dir with data and backups", question_id: Q14, provenance: {kind: inferred, question_id: Q14}}
- {component: outbound, dimension: x-reference-lifecycle-key-rotation, status: claimed, property_id: MX-OUTBOUND-KEY-ROTATION, conditions: "a new key makes stored credentials and encrypted files unreadable", x-routes-to: "§1.13", provenance: {kind: documented, source: 'docs/deployment.md "Stable secrets"'}}
- {component: outbound, dimension: concurrency-reentrancy, status: unresolved, conditions: "loopback guard proxy open to local processes during a capture", question_id: Q19, provenance: {kind: inferred, question_id: Q19}}
- {component: outbound, dimension: resource-complexity, status: unresolved, conditions: "IIIF media up to 4 GB in memory; rclone copies uncapped", question_id: Q16, provenance: {kind: inferred, question_id: Q16}}
# --- Ingest
- {component: ingest, dimension: numeric-domain, status: claimed, property_id: P-UPLOAD, conditions: "uploads.max_mb, 512 MB disk floor, documents.max_pages", provenance: {kind: documented, source: 'docs/configuration.md "512 MB free"'}}
- {component: ingest, dimension: x-numeric-domain-duration-decompressed-size, status: unresolved, conditions: "no duration cap; docx XML read whole", question_id: Q22, provenance: {kind: inferred, question_id: Q22}}
- {component: ingest, dimension: recursive-cyclic-topology, status: unresolved, conditions: "emails inside emails, no stated depth cap", question_id: Q22, provenance: {kind: inferred, question_id: Q22}}
- {component: ingest, dimension: failure-atomicity, status: claimed, property_id: P-UPLOAD, conditions: "an upload file holds whole chunks only", provenance: {kind: documented, source: 'domain/uploads.py docstring "whole chunks"'}}
- {component: ingest, dimension: callback-execution, status: unresolved, conditions: "decoders run in the worker's process tree, unsandboxed", question_id: Q20, provenance: {kind: inferred, question_id: Q20}}
- {component: ingest, dimension: x-process-control, status: claimed, property_id: P-CONVERT-TIMEOUT, conditions: "a conversion or capture outliving its timeout, or children left alive after a kill", provenance: {kind: maintainer, date: "2026-10-06", question_id: Q20}}
- {component: ingest, dimension: serialization-reconstruction, status: claimed, property_id: P-UPLOAD, conditions: "nothing uploaded is executed; served as audio, video or download", provenance: {kind: documented, source: 'docs/configuration.md "uploaded is executed"'}}
- {component: ingest, dimension: reference-lifecycle, status: unresolved, conditions: "temp dirs and process groups cleaned after conversion, best effort", question_id: Q34, provenance: {kind: inferred, question_id: Q34}}
- {component: ingest, dimension: concurrency-reentrancy, status: unresolved, conditions: "jobs run at least once; a retried step may repeat side effects", question_id: Q12, provenance: {kind: inferred, question_id: Q12}}
- {component: ingest, dimension: resource-complexity, status: unresolved, conditions: "some decoders have no timeout; LibreOffice and Chromium get 300 s", question_id: Q22, provenance: {kind: inferred, question_id: Q22}}
- {component: ingest, dimension: x-demuxer-reads, status: unresolved, conditions: "ffmpeg runs without -protocol_whitelist; an upload that makes it read another local file", question_id: Q21, provenance: {kind: inferred, question_id: Q21}}
# --- Query
- {component: query, dimension: numeric-domain, status: claimed, property_id: P-QUERY, conditions: "Cypher: 3 million steps, 8 seconds", provenance: {kind: documented, source: 'docs/graph.md "3 million steps"'}}
- {component: query, dimension: recursive-cyclic-topology, status: claimed, property_id: P-QUERY, conditions: "variable-length paths up to 8 hops", provenance: {kind: documented, source: 'docs/graph.md "3 million steps"'}}
- {component: query, dimension: failure-atomicity, status: not-applicable, conditions: "read-only; engines never change data", x-routes-to: "P-QUERY", provenance: {kind: documented, source: 'domain/cypher.py docstring "it never changes data"'}}
- {component: query, dimension: callback-execution, status: claimed, property_id: P-NO-FETCH, conditions: "SPARQL SERVICE, FROM, graph loading refused", provenance: {kind: documented, source: 'routes/rdf.py sparql docstring "SERVICE and FROM aren''t allowed"'}}
- {component: query, dimension: serialization-reconstruction, status: claimed, property_id: P-NO-FETCH, conditions: "remote JSON-LD contexts refused; RDF/XML not read", provenance: {kind: documented, source: 'routes/rdf.py sparql docstring "SERVICE and FROM aren''t allowed"'}}
- {component: query, dimension: reference-lifecycle, status: not-applicable, conditions: "no saved query state; caches key on the readable set", provenance: {kind: inferred, question_id: Q8}}
# The prose tags this row (assumption, Q19). The v2 schema requires an
# `unresolved` row to carry inferred provenance, so it is recorded one step
# weaker (inferred never closes; assumption could close under relaxed).
- {component: query, dimension: concurrency-reentrancy, status: unresolved, conditions: "SPARQL sets a process-global rdflib flag per call", question_id: Q19, x-prose-provenance-kind: assumption, provenance: {kind: inferred, question_id: Q19}}
- {component: query, dimension: resource-complexity, status: unresolved, conditions: "SPARQL has a row cap only; the Cypher regex guard is heuristic", question_id: Q25, x-also-questions: [Q16], provenance: {kind: inferred, question_id: Q25}}
# --- Assistant
- {component: assistant, dimension: numeric-domain, status: claimed, property_id: P-RETRIEVAL, conditions: "tool loop capped by ai.max_steps and ai.max_transcript_reads", provenance: {kind: documented, source: 'docs/assistant.md "capped by"'}}
- {component: assistant, dimension: failure-atomicity, status: claimed, property_id: P-APPROVAL, conditions: "change tools become approval cards; approval re-checks the role; notes and setup excepted", provenance: {kind: documented, source: 'docs/assistant.md "Approvals"'}}
- {component: assistant, dimension: recursive-cyclic-topology, status: claimed, property_id: P-FLOW, conditions: "flow graphs never loop back; step budget", provenance: {kind: documented, source: 'domain/flow.py docstring "Graphs never loop back"'}}
- {component: assistant, dimension: callback-execution, status: claimed, property_id: P-RETRIEVAL, conditions: "tools see only the person's reads; Python tools from admins only", x-routes-to: "P-RETRIEVAL, P-NO-CODE", provenance: {kind: documented, source: 'docs/assistant.md "limited to what the person can read"'}}
- {component: assistant, dimension: serialization-reconstruction, status: claimed, property_id: P-NO-CODE, conditions: "manifests checked; Python refused for non-admins", provenance: {kind: documented, source: 'domain/extensions.py docstring "Only admins write Python"'}}
- {component: assistant, dimension: x-extension-scope-hooks, status: claimed, property_id: P-EXT-SCOPE, conditions: "another person's hook runs only when an admin shared it", provenance: {kind: documented, source: 'docs/assistant.md "other people''s run only when an admin shared them"'}}
- {component: assistant, dimension: x-extension-scope-tools, status: unresolved, conditions: "another person's tools in my assistant; whether http tool arguments can change its host", question_id: Q27, provenance: {kind: inferred, question_id: Q27}}
- {component: assistant, dimension: reference-lifecycle, status: unresolved, conditions: "saved answers keep text the person can no longer read", question_id: Q11, provenance: {kind: inferred, question_id: Q11}}
- {component: assistant, dimension: concurrency-reentrancy, status: not-applicable, conditions: "one loop per conversation; tool calls run in order", provenance: {kind: inferred, question_id: Q34}}
- {component: assistant, dimension: resource-complexity, status: disclaimed, property_id: N-BUDGET, conditions: "budgets are pre-run estimates, off by default", provenance: {kind: documented, source: 'docs/budgets.md "Budgets are off unless an admin sets one"'}}
- {component: assistant, dimension: x-prompt-injection, status: claimed, property_id: P-INJECTION-EGRESS, conditions: "steering the model within the person's tools is disclaimed (N-PROMPT); carrying what it read out through outbound tools, setup changes or chat-room links asks first or is broken up", provenance: {kind: documented, source: 'docs/assistant.md "it read out of Lens waits for your approval"'}}
- {component: assistant, dimension: x-template-reach, status: unresolved, conditions: "any writer can use a template node in a custom node", question_id: Q23, provenance: {kind: inferred, question_id: Q23}}
# --- Optional
- {component: optional, dimension: numeric-domain, status: unresolved, conditions: "payload 256 KB, 600 per minute per stream, syslog line 64 KB", question_id: Q29, provenance: {kind: inferred, question_id: Q29}}
- {component: optional, dimension: failure-atomicity, status: not-applicable, conditions: "flat, append-only readings stored as data; no multi-step state", provenance: {kind: inferred, question_id: Q34}}
- {component: optional, dimension: recursive-cyclic-topology, status: not-applicable, conditions: "flat readings; no nesting", provenance: {kind: inferred, question_id: Q34}}
- {component: optional, dimension: serialization-reconstruction, status: not-applicable, conditions: "readings stored as data; no object reconstruction", provenance: {kind: inferred, question_id: Q34}}
- {component: optional, dimension: callback-execution, status: claimed, property_id: P-CHILD-SECRETS, conditions: "tunnel token and llama-server key stay off command lines", provenance: {kind: documented, source: 'docs/remote-access.md "in its environment, not on its command line"'}}
- {component: optional, dimension: reference-lifecycle, status: claimed, property_id: P-SENSORS, conditions: "each service off until an admin turns it on", provenance: {kind: documented, source: 'docs/sensors.md "Sensors are off until an admin turns them on"'}}
- {component: optional, dimension: confidentiality, status: claimed, property_id: P-NFS, conditions: "NFS share off until an admin turns it on; read-only; one account's view; allowed networks only", provenance: {kind: documented, source: 'docs/file-shares.md "NFS has no login"'}}
- {component: optional, dimension: confidentiality, status: claimed, property_id: P-SMB, conditions: "SMB share off until an admin turns it on; read-only; each key sees its owner's view; allowed networks only", provenance: {kind: documented, source: 'docs/smb.md "What keeps it safe"'}}
- {component: optional, dimension: concurrency-reentrancy, status: disclaimed, property_id: N-VOLUME, conditions: "MQTT and syslog connection counts, one thread per connection", provenance: {kind: documented, source: 'SECURITY.md "denial of service by sheer volume"'}}
- {component: optional, dimension: resource-complexity, status: disclaimed, property_id: N-VOLUME, conditions: "MQTT and syslog connection counts, one thread per connection", provenance: {kind: documented, source: 'SECURITY.md "denial of service by sheer volume"'}}
# ---------------------------------------------------------------- §1.8
outputs:
- component: authz
channel: "JSON API responses (titles, assets' text, notes, metadata)"
taint: same-as-input
x-taint-note: "as typed by people or produced by ASR/OCR; search snippet is HTML-escaped with <mark>"
taint_provenance: {kind: documented, source: 'docs/api.md "an HTML-escaped `snippet`"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-api-text-safe-for-sinks, statement: "that any text field is safe for HTML, Markdown, SQL or a shell", provenance: {kind: documented, source: 'docs/api.md "an HTML-escaped `snippet`"'}}
- component: authz
channel: "Signed links in responses (api/media.py)"
taint: constrained
taint_provenance: {kind: documented, source: 'docs/architecture.md "is never signed"'}
invariants:
- {id: inv-signed-links-allow-listed, statement: "server-written for allow-listed fields only; text that looks like a link is never signed", property_id: P-MEDIA-LINK, provenance: {kind: documented, source: 'docs/architecture.md "is never signed"'}}
downstream_must_not_assume:
- {id: dmna-signed-link-private, statement: "that a link is private: it is a bearer credential until expiry", provenance: {kind: documented, source: 'docs/architecture.md "is never signed"'}}
- component: authz
channel: "Exports, RDF, JSON-LD, IIIF manifests, WebVTT, Dublin Core"
taint: same-as-input
x-taint-note: "values as typed; RDF responses send Access-Control-Allow-Origin: *"
taint_provenance: {kind: inferred, question_id: Q24}
invariants: []
downstream_must_not_assume:
- {id: dmna-export-escaping, statement: "escaping for the consumer's format", provenance: {kind: inferred, question_id: Q24}}
- component: authz
channel: "Evidence PDFs (evidence.py, evidence_report.py)"
taint: same-as-input
x-taint-note: "the resource's file and what Lens holds about it, for anyone who can read it; places only for members of its namespace; shared notes, highlights and comments only when picked, never private notes; what a model wrote is labelled"
taint_provenance: {kind: documented, source: 'docs/api.md "never anyone''s private notes"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-evidence-pdf-less, statement: "that a PDF handed on holds less than its reader could see (pick its parts)", provenance: {kind: documented, source: 'docs/api.md "never anyone''s private notes"'}}
- component: authz
channel: "Recording description (IIIF, public pages)"
taint: same-as-input
x-taint-note: "never a closed part; anyone who may see the page sees it"
taint_provenance: {kind: documented, source: 'docs/access.md "never a closed part"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-closed-p
8
arts-hid
10BC8
e-description, statement: "that closing parts hides the description", provenance: {kind: documented, source: 'docs/access.md "never a closed part"'}}
- component: authz
channel: "Embed player and /s/<code> pages (api/pages.py)"
taint: same-as-input
x-taint-note: "HTML built from DB values under a strict CSP; framable only per server.embed_frame_ancestors"
taint_provenance: {kind: documented, source: 'docs/configuration.md "Content-Security-Policy"'}
invariants:
- {id: inv-embed-frame-policy, statement: "framable only per server.embed_frame_ancestors, under a strict CSP", property_id: P-FRAME, provenance: {kind: documented, source: 'docs/configuration.md "Content-Security-Policy"'}}
downstream_must_not_assume:
- {id: dmna-titles-inert, statement: "that titles are inert outside this page", provenance: {kind: documented, source: 'docs/configuration.md "Content-Security-Policy"'}}
- component: authz
channel: "Report pages (api/pages.py)"
taint: same-as-input
x-taint-note: "links re-signed on read for every asset of the namespace in the page"
taint_provenance: {kind: inferred, question_id: Q10}
invariants: []
downstream_must_not_assume:
- {id: dmna-report-link-scope, statement: "that a report link opens only the report", provenance: {kind: inferred, question_id: Q10}}
- component: assistant
channel: "Assistant answers, summaries, LLM-written notes"
taint: same-as-input
x-taint-note: "model output over untrusted content"
taint_provenance: {kind: inferred, question_id: Q26}
invariants: []
downstream_must_not_assume:
- {id: dmna-assistant-grounded, statement: "that it is grounded or free of injected instructions", provenance: {kind: inferred, question_id: Q26}}
- component: authz
channel: "MCP tool results (api/mcp_tools.py)"
taint: same-as-input
x-taint-note: "the person's readable data, as text, with chat-template control tokens broken up and a note when text reads like instructions to an AI"
taint_provenance: {kind: documented, source: 'docs/mcp.md "sees exactly what that person sees"'}
invariants:
- {id: inv-mcp-person-reads, statement: "results contain only what the person can read", property_id: P-RETRIEVAL, provenance: {kind: documented, source: 'docs/mcp.md "sees exactly what that person sees"'}}
downstream_must_not_assume:
- {id: dmna-mcp-text-as-instructions, statement: "that an asset's text is safe to follow as instructions", provenance: {kind: documented, source: 'docs/mcp.md "sees exactly what that person sees"'}}
- component: outbound
channel: "Error text from remote servers (LLM, speech, rclone, IMAP callers)"
taint: same-as-input
x-taint-note: "up to 300 characters of the remote reply and the URL as typed"
taint_provenance: {kind: inferred, question_id: Q18}
invariants: []
downstream_must_not_assume:
- {id: dmna-internal-hosts-hidden, statement: "that internal host names stay hidden", provenance: {kind: inferred, question_id: Q18}}
- component: outbound
channel: "Webhook bodies (notify.py)"
taint: same-as-input
x-taint-note: "event data, HMAC-signed for the webhook kind only"
taint_provenance: {kind: documented, source: 'domain/notify.py docstring "signed the Standard Webhooks way"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-chat-webhooks-signed, statement: "that Slack or Discord messages are signed", provenance: {kind: documented, source: 'domain/notify.py docstring "signed the Standard Webhooks way"'}}
- component: outbound
channel: "Fedora export (fedora.py)"
taint: same-as-input
x-taint-note: "every namespace's metadata and files"
taint_provenance: {kind: documented, source: 'domain/fedora.py docstring "every namespace, collection, recording"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-fedora-one-namespace, statement: "that the Fedora account is limited to one namespace", provenance: {kind: documented, source: 'domain/fedora.py docstring "every namespace, collection, recording"'}}
- component: outbound
channel: "Telemetry (telemetry.py)"
taint: constrained
x-taint-note: "route templates, ids, counts; never content"
taint_provenance: {kind: documented, source: 'docs/telemetry.md "Never sent"'}
invariants:
- {id: inv-telemetry-no-content, statement: "never sends content, names, addresses or error messages", property_id: P-TELEMETRY, provenance: {kind: documented, source: 'docs/telemetry.md "Never sent"'}}
downstream_must_not_assume:
- {id: dmna-collector-private, statement: "that the operator's collector is private", provenance: {kind: documented, source: 'docs/telemetry.md "Never sent"'}}
- component: identity
channel: "Server log (API)"
taint: same-as-input
x-taint-note: "setup code and link; sign-in and reset links without mail"
taint_provenance: {kind: documented, source: 'docs/authentication.md "the API prints in its log"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-log-credential-free, statement: "that the log is free of live credentials", provenance: {kind: documented, source: 'docs/authentication.md "the API prints in its log"'}}
- component: identity
channel: "Audit and activity log (auth.audit, activity.py)"
taint: constrained
x-taint-note: "actor, action, target; never prompt or file content"
taint_provenance: {kind: documented, source: 'docs/activity.md "aren''t logged here"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-audit-complete, statement: "completeness or tamper evidence", provenance: {kind: documented, source: 'docs/activity.md "aren''t logged here"'}}
- component: identity
channel: "GET /auth/status"
taint: constrained
x-taint-note: "whether setup is pending and passwords are on, to anyone"
taint_provenance: {kind: documented, source: 'docs/authentication.md endpoint table "`GET /api/v1/auth/status`"'}
invariants: []
downstream_must_not_assume:
- {id: dmna-unset-instance-hidden, statement: "that an unset-up instance is hidden", provenance: {kind: documented, source: 'docs/authentication.md endpoint table "`GET /api/v1/auth/status`"'}}
- component: authz
channel: "Share link failure page (/embed, /s)"
taint: constrained
x-taint-note: "the same 410 for expired, revoked or missing"
taint_provenance: {kind: documented, source: 'docs/authentication.md "the same whatever went wrong"'}
invariants:
- {id: inv-share-uniform-410, statement: "a dead link answers the same 410 whatever went wrong", property_id: P-SHARE, provenance: {kind: documented, source: 'docs/authentication.md "the same whatever went wrong"'}}
downstream_must_not_assume:
- {id: dmna-410-meaningful, statement: "that a 410 says anything about the asset", provenance: {kind: documented, source: 'docs/authentication.md "the same whatever went wrong"'}}
# ---------------------------------------------------------------- §1.10
adversaries:
- name: anonymous-internet-visitor
scope: in
capabilities: [send-http-requests, guess-secrets, send-volume]
excluded_capabilities: [hold-valid-session-link-or-token, database-access, host-access, volume-dos]
goals: [read-archives, claim-admin, obtain-links]
x-note: "volume DoS counts as out (§1.3)"
provenance: {kind: maintainer, date: "2026-10"}
- name: link-holder
scope: in
capabilities: [open-one-path-until-expiry]
excluded_capabilities: [open-other-recordings, write]
goals: [widen-link]
x-note: "share, signed, IIIF probe links"
provenance: {kind: documented, source: 'SECURITY.md "only the server''s own signed links should work"'}
- name: signed-in-person-no-role
scope: in
capabilities: [hold-account, call-api]
excluded_capabilities: [roles-not-given]
goals: [read-other-namespaces]
provenance: {kind: documented, source: 'SECURITY.md "reading or changing anything in a namespace you have no role in"'}
- name: namespace-viewer-editor-owner
scope: in
capabilities: [use-own-role, upload-files, write-extensions, set-notification-targets, write-custom-nodes]
excluded_capabilities: [admin-actions, other-namespaces, host-code]
goals: [cross-namespace-access, ssrf, code-execution]
provenance: {kind: maintainer, date: "2026-10"}
- name: api-token-oauth-app-mcp-agent
scope: in
capabilities: [act-as-person-within-scope]
excluded_capabilities: [administration-via-app-token, write-with-read-token]
goals: [act-beyond-scope]
provenance: {kind: documented, source: 'docs/authentication.md "Tokens act as the person"'}
- name: content-author
scope: in
capabilities: [supply-parsed-bytes, supply-model-input, send-redirects]
excluded_capabilities: [namespace-role, choose-person-tools]
goals: [parser-compromise, ssrf-through-redirects, prompt-injection]
x-note: "uploaded file, transcript, web page, email, calendar, storage remote, IIIF server"
provenance: {kind: inferred, question_id: Q26}
- name: lan-host-or-device
scope: in
capabilities: [send-mqtt-syslog-traffic]
excluded_capabilities: [hub-login-unless-mqtt-anonymous, listed-syslog-network]
goals: [inject-readings, read-readings]
x-note: "only when sensors are on"
provenance: {kind: documented, source: 'docs/sensors.md "Only senders on"'}
- name: network-attacker-on-path
scope: in
capabilities: [observe-alter-traffic-outside-tls]
excluded_capabilities: [break-tls, plain-http-exposed-host]
goals: [steal-sessions]
x-note: "in scope on exposed HTTPS deployments; plain-HTTP exposed hosts count as out (§1.6)"
provenance: {kind: maintainer, date: "2026-10"}
- name: instance-admin
scope: out
capabilities: [everything-including-host-code]
excluded_capabilities: []
goals: []
x-note: "trusted; out of model"
provenance: {kind: maintainer, date: "2026-10"}
- name: host-operator-log-readers-backup-recipients
scope: out
capabilities: [read-env-and-archive-yaml, read-data-dir, read-logs]
excluded_capabilities: []
goals: []
x-note: "trusted. A thief or third party holding a copy of a backup or data_dir is not this actor; such a report escalates under N-KEY-WITH-DATA"
provenance: {kind: maintainer, date: "2026-10"}
- name: worker-host-db-credential-holder
scope: out
capabilities: [full-database]
excluded_capabilities: []
goals: []
x-note: "trusted peer"
provenance: {kind: inferred, question_id: Q9}
- name: upstream-publishers-cloudflare-providers
scope: out
capabilities: [ship-or-relay-bits]
excluded_capabilities: []
goals: []
x-note: "dependency; §1.9"
provenance: {kind: maintainer, date: "2026-10"}
# ---------------------------------------------------------------- §1.9
dependency_policy:
zero_runtime_dependencies: false
x-note: "no vendored third-party source; dependencies come from uv.lock, the pnpm lockfile and container images"
provenance: {kind: inferred, question_id: Q34}
dependencies:
- name: "SurrealDB 3.2.4"
relied_on_for: "parameter binding, transactions, credentials; Lens building queries from client text is in model (P-NO-INJECTION)"
relied_on_properties: [surrealdb-parameter-binding, surrealdb-transactions, surrealdb-credentials]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q45}
- name: "FastAPI, Starlette"
relied_on_for: "HTTP parsing, routing; patched for advisories"
relied_on_properties: [http-parsing, request-routing]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [send-http-requests]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: documented, source: 'CHANGELOG.md "Security: backend dependencies patched"'}
- name: "Next.js, NextAuth"
relied_on_for: "routing, session cookie encryption"
relied_on_properties: [request-routing, session-cookie-encryption]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [send-http-requests]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q45}
- name: "PyJWT, webauthn, cryptography"
relied_on_for: "JWT verification, WebAuthn checks, AES-GCM and HKDF"
relied_on_properties: [jwt-verification, webauthn-verification, aes-gcm-hkdf-correctness]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [send-http-requests]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q45}
- name: rdflib
relied_on_for: "safe Turtle, N-Triples, JSON-LD parsing; upstream for parser bugs; in model if Lens lets it fetch"
relied_on_properties: [rdf-parsing-safety]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [upload-files]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: documented, source: 'rdf_import.py docstring "Nothing is fetched while reading"'}
- name: "Jinja2 SandboxedEnvironment"
relied_on_for: "blocks access to Python internals; upstream for escapes reached from an admin template; Q23 for non-admin reach"
relied_on_properties: [jinja-sandbox-isolation]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: documented, source: 'docs/processing.md "sandboxed Jinja environment"'}
- name: "ffmpeg, LibreOffice, Chromium, poppler, Tesseract, Pillow, pypdf, antiword"
relied_on_for: "parsing hostile media and documents; upstream for parser memory bugs; Lens's arguments, environment and timeouts in model"
relied_on_properties: [hostile-media-parsing]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [upload-files, supply-parsed-bytes]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q20}
- name: "rclone, stdlib urllib, imaplib, ssl"
relied_on_for: "protocol clients and TLS checks"
relied_on_properties: [protocol-client-correctness, tls-certificate-verification]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [supply-parsed-bytes, send-redirects]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q45}
- name: "cloudflared, llama.cpp, hf: models, pip packages"
relied_on_for: "integrity only as far as HTTPS and the publisher; tampered downloads are disclaimed (N-DOWNLOADS)"
relied_on_properties: [https-publisher-integrity]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: maintainer, date: "2026-10"}
- name: "Catalog GGUF models"
relied_on_for: "pinned revision and SHA-256; in model (P-MODEL-CHECKSUM)"
relied_on_properties: [P-MODEL-CHECKSUM]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: true
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: documented, source: 'docs/local-models.md "checked against their SHA-256"'}
- name: "Optional ML extras (whisper, pyannote, speechbrain, insightface, doctr)"
relied_on_for: "model inference; some fetch weights on their own"
relied_on_properties: [model-inference]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: [supply-parsed-bytes]
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q19}
- name: "Build, lint and codegen toolchain (frontend dev dependencies, openapi-ts, linters)"
relied_on_for: "building the images; in no running service; a report must show the code ships"
relied_on_properties: [build-toolchain-integrity]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: inferred, question_id: Q45}
- name: "OIDC providers, SMTP, model and speech providers"
relied_on_for: "honest responses over TLS; the admin chose them; trusted input"
relied_on_properties: [honest-provider-responses]
caller_obligations_acknowledged: []
adversary_capabilities_forwarded: []
outputs_consumed: []
covered_here: false
violation_disposition: "OUT-OF-MODEL: dependency-contract"
provenance: {kind: maintainer, date: "2026-10"}
# ---------------------------------------------------------------- §1.6
build_policy:
security_relevant_flags_present: true
x-note: "support posture, not defaultness, decides routing; shipped defaults are supported on one machine; exposure beyond one machine requires HTTPS and a changed DB credential when SurrealDB runs as its own server"
provenance: {kind: maintainer, date: "2026-10"}
build_flags:
- name: "docker-compose.yml, make dev"
default: "dev secrets, web on 0.0.0.0:3000"
security_relevant: true
maintainer_stance: dev-only
x-scope: "licenses non-default-build only for the known secrets, SURREAL_PASS=root and the web port (§1.3); other dev-stack exposure routes VALID under P-HARDENED until Q36 is answered"
affects_properties:
- {property_id: P-AUTH-REQ, effect: voids}
- {property_id: P-MEDIA-LINK, effect: voids}
- {property_id: P-HARDENED, effect: narrows}
provenance: {kind: maintainer, date: "2026-10"}
- name: "docker-compose.prod.yml, install.sh, Cloudron, Synology, QNAP, Proxmox"
default: "random secrets (Makefile .env writes SURREAL_PASS=root)"
security_relevant: true
maintainer_stance: supported
x-scope: "supported behind HTTPS; both compose files trust the web app container by name (LENS_TRUSTED_PROXY_HOSTS)"
affects_properties:
- {property_id: P-HARDENED, effect: preserves}
- {property_id: P-IPGROUP, effect: preserves}
provenance: {kind: maintainer, date: "2026-10"}
- name: "Proxmox LXC service user"
default: "systemd units have no User=, so Lens runs as root inside the unprivileged LXC; the SurrealDB container runs as root"
security_relevant: true
maintainer_stance: supported
x-stance-status: "unresolved: a report that depends on the service user routes MODEL-GAP, owned by Q33; 'supported' is recorded only so no non-default-build close can fire"
affects_properties:
- {property_id: P-NO-CODE, effect: narrows}