primarysource 0.1.0__py3-none-any.whl

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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
@@ -0,0 +1,304 @@
1
+ """Text views of `service`'s answers: what a tool or a command prints (PLAN.md §5.9).
2
+
3
+ `service` returns plain dicts, which is what `--json` prints. These functions turn the same dicts into `render()`
4
+ text: the answer as lines, then the fixed footer. What a dict carries as a field either stays a line of the body
5
+ (`kev:`, `epss:`, the verdict) or becomes a footer line:
6
+
7
+ - `cite:` the sources that supplied the answer, as `service` names them;
8
+ - `unit:` is `block` when the lines are the numbered items of a list section (a reference, an affected entry, a
9
+ kernel range): each carries its own ref, `CVE-Y-N§references¶9`, and `¶n` on a read means "from item n on";
10
+ - `next:` a ref string for a read (`CVE-Y-N§references¶9`, what `cve_read` takes back as `ref`), `start=<n>` for a
11
+ list (`render.next_start`);
12
+ - `relaxed:` the search matched only some of its terms;
13
+ - `partial:` every source that is missing, failed or stale, because an absence means less when one is (a CVE
14
+ "not in KEV" is a different fact from a KEV catalog that was never synced);
15
+ - `warning:` a hedge the reader must not drop: a truncated description, a version string with a distribution
16
+ suffix.
17
+
18
+ Record text (titles, descriptions, references, KEV notes) is data from other people. It is printed as it came and
19
+ never interpreted; the server instructions say so.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from collections.abc import Mapping
26
+ from typing import Any
27
+
28
+ from primarysource.render import next_start, render
29
+
30
+ PARTIAL_JOIN = " | "
31
+
32
+
33
+ def _footer(payload: Mapping[str, Any], **fixed: str) -> dict[str, str]:
34
+ """The footer lines `payload` implies, plus `fixed` ones. A multi-line value is cut to one line."""
35
+ footer = {key: " ".join(str(value).split()) for key, value in fixed.items() if value}
36
+ gaps = payload.get("partial")
37
+ if gaps:
38
+ footer["partial"] = PARTIAL_JOIN.join(" ".join(str(gap).split()) for gap in gaps)
39
+ return footer
40
+
41
+
42
+ def _next(payload: Mapping[str, Any]) -> str:
43
+ """A read's `next` is already a ref; a list's is `{"start": n}`."""
44
+ value = payload.get("next")
45
+ if isinstance(value, Mapping):
46
+ return next_start(int(value["start"]))
47
+ return str(value) if value else ""
48
+
49
+
50
+ def _score(metric: Mapping[str, Any] | None) -> str:
51
+ if not metric:
52
+ return "none"
53
+ severity = f" {metric['severity']}" if metric.get("severity") else ""
54
+ return f"{metric['score']}{severity} (CVSS {metric['version']}, {metric['label']})"
55
+
56
+
57
+ def _json_line(value: Any) -> str:
58
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
59
+
60
+
61
+ def _kev_line(kev: Mapping[str, Any]) -> str:
62
+ if kev["status"] == "listed":
63
+ return (
64
+ f"listed, added {kev['date_added']}, due {kev['due_date']}, ransomware use {kev['ransomware']}; "
65
+ f"action: {kev['action']}"
66
+ )
67
+ if kev["status"] == "not_listed":
68
+ return f"not listed ({kev['source']})"
69
+ return f"unknown: {kev.get('why', 'not consulted')}"
70
+
71
+
72
+ def _epss_line(epss: Mapping[str, Any]) -> str:
73
+ return f"{epss['score']} (percentile {epss['percentile']}) {epss['source']}"
74
+
75
+
76
+ def _fixed_in(fixed: Mapping[str, str]) -> str:
77
+ return ", ".join(f"{branch} -> {fix}" for branch, fix in fixed.items()) or "none recorded"
78
+
79
+
80
+ # --- info ----------------------------------------------------------------------------------------------------
81
+
82
+
83
+ def info(payload: Mapping[str, Any]) -> str:
84
+ if payload.get("status") == "not_synced":
85
+ body = f"home: {payload['home']}\nindex: not synced yet"
86
+ return render(body, {"warning": f"{payload['error']}; {payload['hint']}; run: {payload['action']}"})
87
+ if "counts" not in payload:
88
+ lines = [f"{payload['ref']} {payload['state']} {payload['title']}"]
89
+ lines.append(f"cvss: {_score(payload['cvss'])}")
90
+ lines.append(f"kev: {payload['kev']}")
91
+ if payload.get("epss"):
92
+ lines.append(f"epss: {_epss_line(payload['epss'])}")
93
+ lines.append(f"kernel CNA data: {'yes' if payload['kernel_cna'] else 'no'}")
94
+ return render("\n".join(lines), _footer(payload, next=str(payload["next"])))
95
+ lines = [f"home: {payload['home']}", f"index: {payload['index']} ({payload['index_mb']} MB)"]
96
+ lines += [f"{name}: {count}" for name, count in payload["counts"].items()]
97
+ lines.append("sources:")
98
+ for name, row in payload["sources"].items():
99
+ lines.append(
100
+ f" {name} {row['state']} version {row['version'] or '-'} upstream {row['upstream_date'] or '-'} "
101
+ f"synced {row['synced_at'] or '-'} records {row['records'] if row['records'] is not None else '-'}"
102
+ + (f" detail: {row['detail']}" if row["detail"] else "")
103
+ )
104
+ return render("\n".join(lines), _footer(payload))
105
+
106
+
107
+ # --- search --------------------------------------------------------------------------------------------------
108
+
109
+
110
+ def search(payload: Mapping[str, Any]) -> str:
111
+ lines: list[str] = []
112
+ for hit in payload["hits"]:
113
+ flags = "".join(
114
+ [
115
+ f" cvss={hit['cvss']}" if "cvss" in hit else "",
116
+ " KEV" if hit.get("kev") else "",
117
+ f" epss={hit['epss']:.3f}" if "epss" in hit else "",
118
+ ]
119
+ )
120
+ lines.append(f"{hit['ref']} [{hit['source']}]{flags} {hit['title']}")
121
+ if hit.get("snippet"):
122
+ lines.append(f" {' '.join(str(hit['snippet']).split())}")
123
+ body = "\n".join(lines) or f"no CVE record matches {payload['query']!r}"
124
+ relaxed = "no record holds every term; these hold some of them" if payload.get("relaxed") and lines else ""
125
+ return render(body, _footer(payload, next=_next(payload), relaxed=relaxed))
126
+
127
+
128
+ # --- read ----------------------------------------------------------------------------------------------------
129
+
130
+
131
+ def _items(payload: Mapping[str, Any], label: str, lines: list[str]) -> list[str]:
132
+ """The numbered items of a list section, each with its own ref so it can be cited."""
133
+ section = str(payload["ref"])
134
+ head = f"{section}: items {payload['start']}-{payload['start'] + len(payload['items']) - 1} of {payload['total']}"
135
+ if not payload["items"]:
136
+ head = f"{section}: no {label}"
137
+ numbered = [f"{section}¶{payload['start'] + offset} {item}" for offset, item in enumerate(payload["items"])]
138
+ return [head, *lines, *numbered]
139
+
140
+
141
+ def _affected_line(entry: Any) -> str:
142
+ if not isinstance(entry, Mapping):
143
+ return str(entry)
144
+ head = f"{entry.get('vendor', '')} {entry.get('product', '')}".strip()
145
+ ranges: list[str] = []
146
+ for version in entry.get("versions") or []:
147
+ if not isinstance(version, Mapping):
148
+ continue
149
+ bits = [str(version.get("status", "")), str(version.get("version", ""))]
150
+ bits += [f"{key} {version[key]}" for key in ("lessThan", "lessThanOrEqual") if version.get(key)]
151
+ ranges.append(" ".join(bit for bit in bits if bit))
152
+ if ranges:
153
+ return f"{head}: {'; '.join(ranges)}"
154
+ return head or _json_line(entry)
155
+
156
+
157
+ def read(payload: Mapping[str, Any]) -> str:
158
+ ref = str(payload["ref"])
159
+ if payload.get("state") == "UNKNOWN_TO_CVE_SOURCES":
160
+ lines = [f"{ref} {payload['note']}", f"kev: {_kev_line(payload['kev'])}"]
161
+ if payload.get("epss"):
162
+ lines.append(f"epss: {_epss_line(payload['epss'])}")
163
+ return render("\n".join(lines), _footer(payload))
164
+ cite = str(payload.get("cite", ""))
165
+ if "§" in ref:
166
+ section = ref.split("§", 1)[1]
167
+ if section == "references":
168
+ body = "\n".join(_items(payload, "references", []))
169
+ return render(body, _footer(payload, cite=cite, unit="block", next=_next(payload)))
170
+ if section == "affected":
171
+ lines = _items(
172
+ {**payload, "items": [_affected_line(i) for i in payload["items"]]}, "affected entries", []
173
+ )
174
+ return render("\n".join(lines), _footer(payload, cite=cite, unit="block", next=_next(payload)))
175
+ if section == "cvss":
176
+ lines = [f"{ref} preferred: {_score(payload['preferred'])}"]
177
+ for metric in payload["items"]:
178
+ lines.append(f"{metric['label']} {metric['version']} {_score(metric)} {metric['vector']}")
179
+ if not payload["items"]:
180
+ lines.append("no CVSS score in any synced source")
181
+ return render("\n".join(lines), _footer(payload, cite=cite))
182
+ if section == "kernel":
183
+ if "items" not in payload:
184
+ return render(f"{ref} {payload['note']}", _footer(payload, cite=cite))
185
+ lines = [
186
+ f"fixed in: {_fixed_in(payload['fixed_in'])}",
187
+ f"unfixed on start: {', '.join(payload['unfixed_on_start']) or 'none'}",
188
+ f"source: {payload['source']}",
189
+ ]
190
+ body = "\n".join(_items(payload, "kernel ranges", lines))
191
+ return render(body, _footer(payload, cite=cite, unit="block", next=_next(payload)))
192
+ lines = [f"{ref}"]
193
+ for name, row in payload["sources"].items():
194
+ lines.append(
195
+ f"{name} {row['state']} version {row['version'] or '-'} upstream {row['upstream_date'] or '-'} "
196
+ f"synced {row['synced_at'] or '-'}"
197
+ )
198
+ return render("\n".join(lines), _footer(payload, cite=cite))
199
+ return _summary(payload)
200
+
201
+
202
+ def _summary(payload: Mapping[str, Any]) -> str:
203
+ lines = [f"{payload['ref']} {payload['state']}"]
204
+ if payload["title"]:
205
+ lines.append(f"title: {payload['title']}")
206
+ lines.append(
207
+ f"assigner: {payload['assigner'] or '-'} published: {payload['published']} updated: {payload['updated']}"
208
+ )
209
+ lines.append(f"cwe: {', '.join(payload['cwe']) or '-'}")
210
+ lines.append(
211
+ f"cvss: {_score(payload['cvss']['preferred'])}; {payload['cvss']['count']} score(s) in {payload['ref']}§cvss"
212
+ )
213
+ lines.append(f"kev: {_kev_line(payload['kev'])}")
214
+ if payload.get("epss"):
215
+ lines.append(f"epss: {_epss_line(payload['epss'])}")
216
+ if payload.get("ssvc"):
217
+ lines.append("ssvc: " + ", ".join(f"{key}={value}" for key, value in payload["ssvc"].items()))
218
+ kernel = payload.get("kernel")
219
+ if kernel:
220
+ lines.append(f"kernel: {kernel['source']}; fixed in {_fixed_in(kernel['fixed_in'])}")
221
+ if kernel["unfixed_on_start"]:
222
+ lines.append(f"kernel unfixed on start: {', '.join(kernel['unfixed_on_start'])}")
223
+ if kernel["files"]:
224
+ more = kernel["files_total"] - len(kernel["files"])
225
+ lines.append(f"kernel files: {' '.join(kernel['files'])}" + (f" (+{more} more)" if more > 0 else ""))
226
+ lines.append(f"kernel question: cve_kernel('{payload['ref']}', '<version>')")
227
+ lines.append(f"description ({payload['description_source']}):")
228
+ lines.append(str(payload["description"]))
229
+ lines.append(f"references ({len(payload['references'])} of {payload['references_total']}):")
230
+ lines += [f" {url}" for url in payload["references"]]
231
+ lines.append("sections: " + ", ".join(payload["sections"]))
232
+ warning = "description and references cut to fit max_tokens; raise max_tokens" if payload.get("truncated") else ""
233
+ return render("\n".join(lines), _footer(payload, cite=str(payload["cite"]), next=_next(payload), warning=warning))
234
+
235
+
236
+ # --- kev -----------------------------------------------------------------------------------------------------
237
+
238
+
239
+ def kev(payload: Mapping[str, Any]) -> str:
240
+ if "status" in payload:
241
+ if payload["status"] == "not_listed":
242
+ body = f"{payload['ref']} is not in the CISA KEV catalog"
243
+ return render(body, _footer(payload, cite=str(payload["source"])))
244
+ lines = [
245
+ f"{payload['ref']} listed in CISA KEV",
246
+ f"vendor: {payload['vendor']} product: {payload['product']}",
247
+ f"name: {payload['name']}",
248
+ f"added: {payload['date_added']} due: {payload['due_date']} ransomware use: {payload['ransomware']}",
249
+ f"action: {payload['action']}",
250
+ f"description: {payload['description']}",
251
+ ]
252
+ if payload.get("notes"):
253
+ lines.append(f"notes: {payload['notes']}")
254
+ if payload.get("cwes"):
255
+ lines.append(f"cwe: {', '.join(payload['cwes'])}")
256
+ if payload.get("forensic_triage"):
257
+ lines.append(f"forensic triage: {payload['forensic_triage']}")
258
+ return render("\n".join(lines), _footer(payload, cite=str(payload["source"])))
259
+ lines = [f"{payload['total']} KEV entries, newest first"]
260
+ lines += [
261
+ f"{row['cve']} added {row['date_added']} due {row['due_date']} {row['vendor']}/{row['product']} "
262
+ f"{row['name']}" + (f" ransomware: {row['ransomware']}" if row["ransomware"] else "")
263
+ for row in payload["items"]
264
+ ]
265
+ return render("\n".join(lines), _footer(payload, cite=str(payload["source"]), next=_next(payload)))
266
+
267
+
268
+ # --- kernel --------------------------------------------------------------------------------------------------
269
+
270
+
271
+ def _caveats(payload: Mapping[str, Any]) -> str:
272
+ return PARTIAL_JOIN.join(payload.get("caveats", []))
273
+
274
+
275
+ def kernel_check(payload: Mapping[str, Any]) -> str:
276
+ confidence = f" (confidence: {payload['confidence']})" if payload.get("confidence") else ""
277
+ lines = [f"{payload['ref']} on kernel {payload['version']}: {payload['verdict']}{confidence}"]
278
+ if payload.get("reason"):
279
+ lines.append(f"reason: {payload['reason']}")
280
+ if payload.get("detail"):
281
+ lines.append(f"detail: {payload['detail']}")
282
+ lines += [f"evidence: {item}" for item in payload.get("evidence", [])]
283
+ if payload.get("fixed_in"):
284
+ lines.append(f"fixed in: {_fixed_in(payload['fixed_in'])}")
285
+ if payload.get("note"):
286
+ lines.append(f"note: {payload['note']}")
287
+ return render("\n".join(lines), _footer(payload, cite=str(payload.get("source", "")), warning=_caveats(payload)))
288
+
289
+
290
+ def kernel_list(payload: Mapping[str, Any]) -> str:
291
+ lines = [f"{payload['version']}: {payload['affected_total']} of {payload['assessed']} kernel CNA CVEs affect it"]
292
+ for item in payload["items"]:
293
+ marks = "".join([" KEV" if item.get("kev") else "", f" epss={item['epss']:.3f}" if "epss" in item else ""])
294
+ files = f" files: {' '.join(item['files'])}" if item.get("files") else ""
295
+ lines.append(f"{item['ref']}{marks} {item['title']} ({item['why']}){files}")
296
+ lines.append(f"note: {payload['note']}")
297
+ return render(
298
+ "\n".join(lines),
299
+ _footer(payload, cite=str(payload["source"]), next=_next(payload), warning=_caveats(payload)),
300
+ )
301
+
302
+
303
+ def path(home: str, index: str) -> str:
304
+ return render(f"home: {home}\nindex: {index}\nsetting: CVELIB_HOME or PRIMARYSOURCE_CVE_HOME", {})
@@ -0,0 +1,16 @@
1
+ """`debpolicy`: Debian Policy and the dpkg manual pages, pinned releases, read as reStructuredText and POD.
2
+ Served under `debpolicy_`.
3
+
4
+ Stdlib only. `mcp` is imported by `server.py` when a server is built; `primarysource debpolicy ...` runs `cli.py`.
5
+ `docs/debpolicy.md` is the description; `catalogue.py` holds the pin, the licence and the build.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from primarysource.debpolicy.catalogue import SPEC
11
+ from primarysource.pkgdocs.runtime import make_collection
12
+
13
+ COLLECTION = make_collection(SPEC, "primarysource.debpolicy")
14
+ """Refs: `debpolicy:5.6.1`; see `pkgdocs.runtime`."""
15
+
16
+ __all__ = ["COLLECTION", "SPEC"]
@@ -0,0 +1,263 @@
1
+ """The `debpolicy` collection: Debian Policy (reStructuredText) and the dpkg manual pages (POD).
2
+
3
+ Two pinned trees, so two editions that move independently:
4
+
5
+ - `policy`: dbnpolicy/policy on salsa.debian.org at the commit of the tag `debian/4.7.4.1`. The documents are
6
+ `policy/*.rst` (chapters `ch-*`, appendices `ap-*`, `upgrading-checklist`, `index`) and `autopkgtest.md`;
7
+ - `dpkg`: dpkg-team/dpkg at the tag `1.23.12`. The documents are the 63 `man/*.pod` pages, with their manual
8
+ section taken from `man/Makefile.am` (`man_MANS`). In 1.23.12 the pages are POD, not roff; roff appears only
9
+ after `pod2man`.
10
+
11
+ Licence (read from the repositories on 2026-11-13, see `docs/debpolicy.md`): the policy tree's `debian/copyright`
12
+ (DEP-5) gives `Files: *` as GPL-2+ and names the exceptions (`copyright-format*.xml`, `debconf*`, `fhs-3.0*` and
13
+ some diagrams), none of which is imported; the build reads that file and puts the matching `License:` of each
14
+ imported document in its meta. Each dpkg page carries its own header, "either version 2 of the License, or (at
15
+ your option) any later version", which the POD build reads the same way. Placement is **layer**.
16
+
17
+ Numbering: a chapter's number is its place in the first `toctree` of `index.rst`, so `ch-controlfields` is
18
+ chapter 5 and its subsections are `5.1`, `5.6.1`. `debpolicy:5.6.1` works without a document id for that
19
+ reason. Appendices are numbered locally inside their own document (the build does not claim Sphinx's appendix
20
+ numbering, which was not checked).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import fnmatch
26
+ import re
27
+ from collections import Counter
28
+ from collections.abc import Mapping
29
+
30
+ from primarysource.errors import LibError
31
+ from primarysource.pkgdocs.fetch import Source
32
+ from primarysource.pkgdocs.model import Doc
33
+ from primarysource.pkgdocs.readers._common import decode
34
+ from primarysource.pkgdocs.readers.markdown import read_markdown
35
+ from primarysource.pkgdocs.readers.pod import read_pod
36
+ from primarysource.pkgdocs.readers.rst import read_rst
37
+ from primarysource.pkgdocs.runtime import Build, Licence, Pin, Spec, TreeSpec, matching
38
+
39
+ POLICY_COMMIT = "aceb1daf7305adc0b336ee2ebbdf0bc0340d3b68"
40
+ DPKG_COMMIT = "f78cedcdcbbf25b27f3104b99ac3d2832e19d0cc"
41
+
42
+ POLICY_PIN = Pin(
43
+ commit=POLICY_COMMIT,
44
+ label="4.7.4.1",
45
+ date="2026-03-31",
46
+ files_sha256="bcf7ce3c75512efe7274fedf88a8c615483e50872c9fd644c678a816f8b3319b",
47
+ sources=(
48
+ Source(
49
+ f"https://salsa.debian.org/dbnpolicy/policy/-/archive/{POLICY_COMMIT}/policy-{POLICY_COMMIT}.tar.gz",
50
+ strip=1,
51
+ ),
52
+ ),
53
+ )
54
+ DPKG_PIN = Pin(
55
+ commit=DPKG_COMMIT,
56
+ label="1.23.12",
57
+ date="2026-11-13",
58
+ files_sha256="22d4c9ed26c64e136510ae6d7f002b7afb309613223bc88ef025413b8b47e610",
59
+ sources=(
60
+ Source(f"https://salsa.debian.org/dpkg-team/dpkg/-/archive/{DPKG_COMMIT}/dpkg-{DPKG_COMMIT}.tar.gz", strip=1),
61
+ Source(
62
+ f"https://github.com/guillemj/dpkg/archive/{DPKG_COMMIT}.tar.gz",
63
+ strip=1,
64
+ hosts=("codeload.github.com",),
65
+ ),
66
+ ),
67
+ )
68
+
69
+ LICENCE = Licence(
70
+ spdx="GPL-2.0-or-later",
71
+ redistribute="layer",
72
+ attribution="Debian Policy: Ian Jackson, Christian Schwarz and many other Debian contributors; "
73
+ "dpkg: Guillem Jover and others",
74
+ evidence="debian/copyright (`Files: *`, License: GPL-2+) in the policy tree; the header of each dpkg .pod",
75
+ )
76
+
77
+ _GPL2_PLUS = "GPL-2.0-or-later"
78
+ _MAN_MANS = re.compile(r"man_MANS\s*\+?=\s*((?:[^\n]*\\\n)*[^\n]*)")
79
+ _PAGE = re.compile(r"([\w.+-]+)\.(\d)")
80
+
81
+
82
+ def _copyright_stanzas(text: str) -> list[tuple[list[str], str]]:
83
+ """(file patterns, licence short name) for each DEP-5 `Files:` stanza of a `debian/copyright`."""
84
+ stanzas: list[tuple[list[str], str]] = []
85
+ for block in re.split(r"\n\s*\n", text):
86
+ files: list[str] = []
87
+ licence = ""
88
+ in_files = False
89
+ for line in block.split("\n"):
90
+ if line.startswith("Files:"):
91
+ in_files = True
92
+ files.extend(line[len("Files:") :].split())
93
+ elif in_files and line[:1] in (" ", "\t"):
94
+ files.extend(line.split())
95
+ else:
96
+ in_files = False
97
+ if line.startswith("License:"):
98
+ licence = line[len("License:") :].strip()
99
+ if files and licence:
100
+ stanzas.append((files, licence))
101
+ return stanzas
102
+
103
+
104
+ def licence_of(path: str, stanzas: list[tuple[list[str], str]]) -> str:
105
+ """The licence DEP-5 gives `path`: the last `Files:` stanza that matches it (`*` also matches `/`)."""
106
+ found = ""
107
+ for patterns, licence in stanzas:
108
+ if any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns):
109
+ found = licence
110
+ return found
111
+
112
+
113
+ def _chapter_order(index: bytes | None) -> list[str]:
114
+ """Document stems in the first `toctree` of `index.rst`, in order."""
115
+ if index is None:
116
+ return []
117
+ stems: list[str] = []
118
+ in_tree = False
119
+ for line in decode(index, "policy/index.rst").split("\n"):
120
+ if line.startswith(".. toctree::"):
121
+ if stems:
122
+ break
123
+ in_tree = True
124
+ elif in_tree and line.strip() and line.startswith((" ", "\t")) and not line.strip().startswith(":"):
125
+ stems.append(line.strip())
126
+ elif in_tree and line.strip() and not line.startswith((" ", "\t")):
127
+ break
128
+ return stems
129
+
130
+
131
+ def build_policy(files: Mapping[str, bytes], pin: Pin) -> Build:
132
+ stanzas = (
133
+ _copyright_stanzas(decode(files["debian/copyright"], "debian/copyright"))
134
+ if "debian/copyright" in files
135
+ else []
136
+ )
137
+ order = _chapter_order(files.get("policy/index.rst"))
138
+ docs: list[Doc] = []
139
+ aliases: dict[str, str] = {}
140
+ skipped: Counter[str] = Counter()
141
+ if not stanzas:
142
+ skipped["debian/copyright not found or has no Files stanza: licences unchecked"] += 1
143
+ for path in sorted(files):
144
+ if path == "debian/copyright":
145
+ continue
146
+ stem = path.rsplit("/", 1)[-1].rsplit(".", 1)[0]
147
+ try:
148
+ if path.startswith("policy/") and path.endswith(".rst"):
149
+ number = str(order.index(stem) + 1) if stem.startswith("ch-") and stem in order else ""
150
+ doc = read_rst(files[path], stem, base=number)
151
+ if number:
152
+ aliases[number] = stem
153
+ doc = Doc(
154
+ doc.id,
155
+ f"Debian Policy {pin.label} chapter {number}: {doc.title}",
156
+ doc.unit,
157
+ doc.sections,
158
+ doc.meta,
159
+ doc.warnings,
160
+ )
161
+ elif stem != "index":
162
+ doc = Doc(
163
+ doc.id,
164
+ f"Debian Policy {pin.label}: {doc.title}",
165
+ doc.unit,
166
+ doc.sections,
167
+ doc.meta,
168
+ doc.warnings,
169
+ )
170
+ elif path.endswith(".md"):
171
+ doc = read_markdown(files[path], stem)
172
+ else:
173
+ skipped["not a policy document"] += 1
174
+ continue
175
+ except LibError:
176
+ skipped["not UTF-8 text"] += 1
177
+ continue
178
+ found = licence_of(path, stanzas)
179
+ docs.append(doc.with_meta(path=path, licence=_GPL2_PLUS if found == "GPL-2+" else (found or "unchecked")))
180
+ return Build(tuple(docs), aliases, skipped)
181
+
182
+
183
+ def man_sections(makefile: bytes | None) -> dict[str, str]:
184
+ """`{page: section}` from the `man_MANS` lists of `man/Makefile.am` (`deb-control.5` -> `deb-control: 5`)."""
185
+ if makefile is None:
186
+ return {}
187
+ sections: dict[str, str] = {}
188
+ for match in _MAN_MANS.finditer(decode(makefile, "man/Makefile.am")):
189
+ for token in match.group(1).replace("\\\n", " ").split():
190
+ found = _PAGE.fullmatch(token)
191
+ if found:
192
+ sections[found.group(1)] = found.group(2)
193
+ return sections
194
+
195
+
196
+ def build_dpkg(files: Mapping[str, bytes], pin: Pin) -> Build:
197
+ sections = man_sections(files.get("man/Makefile.am"))
198
+ docs: list[Doc] = []
199
+ aliases: dict[str, str] = {}
200
+ skipped: Counter[str] = Counter()
201
+ for path in sorted(files):
202
+ if not (path.startswith("man/") and path.endswith(".pod")):
203
+ continue
204
+ page = path[len("man/") : -len(".pod")]
205
+ try:
206
+ doc = read_pod(files[path], page)
207
+ except LibError:
208
+ skipped["not UTF-8 text"] += 1
209
+ continue
210
+ number = sections.get(page, "")
211
+ if not number:
212
+ skipped["page not listed in man_MANS: no manual section"] += 1
213
+ _, _, description = doc.title.partition(" - ")
214
+ title = f"{page}({number})" + (f" - {description}" if description else "") if number else doc.title
215
+ text = decode(files[path], path)
216
+ licence = (
217
+ _GPL2_PLUS
218
+ if "either version 2 of the License, or" in text and "any later version" in text
219
+ else "see file"
220
+ )
221
+ doc = Doc(doc.id, title, doc.unit, doc.sections, doc.meta, doc.warnings)
222
+ docs.append(doc.with_meta(path=path, licence=licence, man_section=number))
223
+ if number:
224
+ aliases[f"{page}.{number}"] = page
225
+ return Build(tuple(docs), aliases, skipped)
226
+
227
+
228
+ SPEC = Spec(
229
+ name="debpolicy",
230
+ title="Debian Policy and the dpkg manual pages",
231
+ licence=LICENCE,
232
+ trees=(
233
+ TreeSpec(
234
+ name="policy",
235
+ title="Debian Policy Manual (policy/*.rst, autopkgtest.md)",
236
+ pins=(POLICY_PIN,),
237
+ include=matching(r"policy/[a-z0-9-]+\.rst|autopkgtest\.md|debian/copyright"),
238
+ build=build_policy,
239
+ ),
240
+ TreeSpec(
241
+ name="dpkg",
242
+ title="dpkg manual pages (man/*.pod)",
243
+ pins=(DPKG_PIN,),
244
+ include=matching(r"man/[A-Za-z0-9.+-]+\.pod|man/Makefile\.am"),
245
+ build=build_dpkg,
246
+ ),
247
+ ),
248
+ instructions=(
249
+ "Debian Policy (chapters 1-12 and the appendices) and the dpkg manual pages (deb(5), deb-control(5), "
250
+ "dpkg-deb(1), ...) from the user's imported copies at pinned releases. Loop: debpolicy_search('<words>') -> "
251
+ "debpolicy_outline('<doc>') -> debpolicy_read('<ref>'). Policy section numbers work as refs: '5.6.1' is "
252
+ "the Source field of chapter 5 (ch-controlfields); chapters are the documents ch-* and appendices ap-*. "
253
+ "A dpkg page's document id is its name ('deb-control'); 'deb-control.5' works too. A section is an id, an "
254
+ "anchor (Policy's s-f-Source), a heading or a unique part of one. If nothing is imported the error says "
255
+ "how."
256
+ ),
257
+ example_refs=(
258
+ "debpolicy:5.6.1",
259
+ "debpolicy:ch-controlfields§s-f-Source",
260
+ "debpolicy:deb-control§DESCRIPTION",
261
+ "debpolicy:5.6.1@aceb1daf¶2",
262
+ ),
263
+ )
@@ -0,0 +1,12 @@
1
+ """`primarysource debpolicy import|status|docs|outline|read|search|cite`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from primarysource.debpolicy.catalogue import SPEC
8
+ from primarysource.pkgdocs.serving import run
9
+
10
+
11
+ def main(argv: Sequence[str]) -> int:
12
+ return run(SPEC, argv)
@@ -0,0 +1,11 @@
1
+ """The MCP server for `debpolicy` (`primarysource mcp --collection debpolicy`).
2
+
3
+ Imports `mcp` when this module loads.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from primarysource.debpolicy.catalogue import SPEC
9
+ from primarysource.pkgdocs.serving import build_server
10
+
11
+ server = build_server(SPEC)
@@ -0,0 +1,11 @@
1
+ """IETF Internet-Drafts as a primarysource collection: `primarysource mcp --collection draft`.
2
+
3
+ The portfolio tracks documents that exist only as drafts (RTR version 2 and the ASPA PDU, ASPA verification,
4
+ LibrePGP, the TURN REST API), which the RFC library cannot cite. This collection imports named drafts from the
5
+ IETF's own servers into `$PRIMARYSOURCE_HOME/draft/` and serves them like the `rfc` collection does, with one
6
+ difference that matters: a draft has revisions and a life cycle, so every result says which revision it read and
7
+ whether that draft is still alive.
8
+
9
+ `docs/draft.md` is the user-facing description. Nothing here needs a dependency beyond the standard library;
10
+ serving needs the `[mcp]` extra. `primarysource draft ...` runs `cli.py`.
11
+ """