@intentius/chant-lexicon-cpln 0.53.0

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 (266) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +77 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/codegen/docs-cli.d.ts +3 -0
  5. package/dist/codegen/docs-cli.d.ts.map +1 -0
  6. package/dist/codegen/docs.d.ts +17 -0
  7. package/dist/codegen/docs.d.ts.map +1 -0
  8. package/dist/codegen/generate-cli.d.ts +6 -0
  9. package/dist/codegen/generate-cli.d.ts.map +1 -0
  10. package/dist/codegen/generate-lexicon.d.ts +36 -0
  11. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  12. package/dist/codegen/generate-typescript.d.ts +11 -0
  13. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  14. package/dist/codegen/generate.d.ts +15 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/naming.d.ts +30 -0
  17. package/dist/codegen/naming.d.ts.map +1 -0
  18. package/dist/codegen/package.d.ts +17 -0
  19. package/dist/codegen/package.d.ts.map +1 -0
  20. package/dist/composites/cron-job.d.ts +60 -0
  21. package/dist/composites/cron-job.d.ts.map +1 -0
  22. package/dist/composites/gvc-environment.d.ts +47 -0
  23. package/dist/composites/gvc-environment.d.ts.map +1 -0
  24. package/dist/composites/index.d.ts +13 -0
  25. package/dist/composites/index.d.ts.map +1 -0
  26. package/dist/composites/public-domain.d.ts +68 -0
  27. package/dist/composites/public-domain.d.ts.map +1 -0
  28. package/dist/composites/secret-access.d.ts +78 -0
  29. package/dist/composites/secret-access.d.ts.map +1 -0
  30. package/dist/composites/serverless-service.d.ts +78 -0
  31. package/dist/composites/serverless-service.d.ts.map +1 -0
  32. package/dist/composites/stateful-service.d.ts +79 -0
  33. package/dist/composites/stateful-service.d.ts.map +1 -0
  34. package/dist/coverage.d.ts +59 -0
  35. package/dist/coverage.d.ts.map +1 -0
  36. package/dist/describe-resources.d.ts +71 -0
  37. package/dist/describe-resources.d.ts.map +1 -0
  38. package/dist/detect.d.ts +14 -0
  39. package/dist/detect.d.ts.map +1 -0
  40. package/dist/entity-props.d.ts +34 -0
  41. package/dist/entity-props.d.ts.map +1 -0
  42. package/dist/generated/index.d.ts +151 -0
  43. package/dist/generated/index.d.ts.map +1 -0
  44. package/dist/generated/runtime.d.ts +5 -0
  45. package/dist/generated/runtime.d.ts.map +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/init-templates.d.ts +16 -0
  49. package/dist/init-templates.d.ts.map +1 -0
  50. package/dist/integrity.json +35 -0
  51. package/dist/kinds.d.ts +70 -0
  52. package/dist/kinds.d.ts.map +1 -0
  53. package/dist/lint/audit-catalog.d.ts +19 -0
  54. package/dist/lint/audit-catalog.d.ts.map +1 -0
  55. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
  56. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
  57. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
  58. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
  59. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
  60. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
  61. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
  62. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
  63. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
  64. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
  66. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
  68. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
  70. package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
  72. package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
  74. package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
  76. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
  78. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
  80. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
  81. package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
  82. package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
  83. package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
  84. package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
  85. package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
  86. package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
  87. package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
  88. package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
  89. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
  90. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
  91. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
  92. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
  93. package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
  94. package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
  95. package/dist/lint/post-synth/helpers.d.ts +45 -0
  96. package/dist/lint/post-synth/helpers.d.ts.map +1 -0
  97. package/dist/lint/post-synth/index.d.ts +3 -0
  98. package/dist/lint/post-synth/index.d.ts.map +1 -0
  99. package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
  100. package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
  101. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
  102. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
  103. package/dist/lint/rules/index.d.ts +6 -0
  104. package/dist/lint/rules/index.d.ts.map +1 -0
  105. package/dist/lsp/completions.d.ts +7 -0
  106. package/dist/lsp/completions.d.ts.map +1 -0
  107. package/dist/lsp/hover.d.ts +4 -0
  108. package/dist/lsp/hover.d.ts.map +1 -0
  109. package/dist/manifest.json +6 -0
  110. package/dist/meta.json +2110 -0
  111. package/dist/okf/index.md +39 -0
  112. package/dist/okf/rules/CPL001.md +22 -0
  113. package/dist/okf/rules/CPL002.md +22 -0
  114. package/dist/okf/rules/CPL010.md +15 -0
  115. package/dist/okf/rules/CPL011.md +15 -0
  116. package/dist/okf/rules/CPL012.md +15 -0
  117. package/dist/okf/rules/CPL013.md +15 -0
  118. package/dist/okf/rules/CPL014.md +16 -0
  119. package/dist/okf/rules/CPL020.md +11 -0
  120. package/dist/okf/rules/CPL021.md +15 -0
  121. package/dist/okf/rules/CPL022.md +15 -0
  122. package/dist/okf/rules/CPL023.md +15 -0
  123. package/dist/okf/rules/CPL024.md +15 -0
  124. package/dist/okf/rules/CPL025.md +15 -0
  125. package/dist/okf/rules/CPL026.md +15 -0
  126. package/dist/okf/rules/CPL027.md +15 -0
  127. package/dist/okf/rules/CPL028.md +15 -0
  128. package/dist/okf/rules/CPL029.md +15 -0
  129. package/dist/okf/rules/CPL030.md +16 -0
  130. package/dist/okf/rules/CPL040.md +15 -0
  131. package/dist/okf/rules/CPL041.md +15 -0
  132. package/dist/okf/rules/CPL042.md +11 -0
  133. package/dist/okf/rules/CPL043.md +15 -0
  134. package/dist/okf/types/Domain.md +22 -0
  135. package/dist/okf/types/Gvc.md +21 -0
  136. package/dist/okf/types/Identity.md +29 -0
  137. package/dist/okf/types/IpSet.md +21 -0
  138. package/dist/okf/types/Policy.md +28 -0
  139. package/dist/okf/types/Secret.md +23 -0
  140. package/dist/okf/types/VolumeSet.md +22 -0
  141. package/dist/okf/types/Workload.md +38 -0
  142. package/dist/ownership.d.ts +23 -0
  143. package/dist/ownership.d.ts.map +1 -0
  144. package/dist/package-cli.d.ts +3 -0
  145. package/dist/package-cli.d.ts.map +1 -0
  146. package/dist/plugin.d.ts +3 -0
  147. package/dist/plugin.d.ts.map +1 -0
  148. package/dist/reference-catalog.d.ts +24 -0
  149. package/dist/reference-catalog.d.ts.map +1 -0
  150. package/dist/rules/cpl001-no-secret-literals.ts +83 -0
  151. package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
  152. package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
  153. package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
  154. package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
  155. package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
  156. package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
  157. package/dist/rules/cpl020-serverless-single-port.ts +58 -0
  158. package/dist/rules/cpl021-cron-job-shape.ts +64 -0
  159. package/dist/rules/cpl022-container-ports.ts +64 -0
  160. package/dist/rules/cpl023-container-resources.ts +87 -0
  161. package/dist/rules/cpl024-probe-handler.ts +51 -0
  162. package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
  163. package/dist/rules/cpl026-scale-to-zero.ts +67 -0
  164. package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
  165. package/dist/rules/cpl028-volume-set.ts +183 -0
  166. package/dist/rules/cpl029-link-targets.ts +155 -0
  167. package/dist/rules/cpl030-domain-routing.ts +137 -0
  168. package/dist/rules/cpl040-pinned-image.ts +48 -0
  169. package/dist/rules/cpl041-image-reference-form.ts +60 -0
  170. package/dist/rules/cpl042-gvc-placement.ts +57 -0
  171. package/dist/rules/cpl043-policy-scope.ts +0 -0
  172. package/dist/rules/helpers.ts +114 -0
  173. package/dist/serializer.d.ts +69 -0
  174. package/dist/serializer.d.ts.map +1 -0
  175. package/dist/skills/chant-cpln-secrets.md +110 -0
  176. package/dist/skills/chant-cpln-workloads.md +78 -0
  177. package/dist/skills/chant-cpln.md +103 -0
  178. package/dist/spec/fetch.d.ts +43 -0
  179. package/dist/spec/fetch.d.ts.map +1 -0
  180. package/dist/spec/parse.d.ts +79 -0
  181. package/dist/spec/parse.d.ts.map +1 -0
  182. package/dist/spec/snapshot-cli.d.ts +18 -0
  183. package/dist/spec/snapshot-cli.d.ts.map +1 -0
  184. package/dist/types/index.d.ts +1649 -0
  185. package/dist/validate-cli.d.ts +3 -0
  186. package/dist/validate-cli.d.ts.map +1 -0
  187. package/dist/validate.d.ts +26 -0
  188. package/dist/validate.d.ts.map +1 -0
  189. package/package.json +74 -0
  190. package/src/api.ts +134 -0
  191. package/src/codegen/docs-cli.ts +7 -0
  192. package/src/codegen/docs.ts +94 -0
  193. package/src/codegen/generate-cli.ts +34 -0
  194. package/src/codegen/generate-lexicon.ts +88 -0
  195. package/src/codegen/generate-typescript.ts +71 -0
  196. package/src/codegen/generate.ts +110 -0
  197. package/src/codegen/naming.ts +61 -0
  198. package/src/codegen/package.ts +35 -0
  199. package/src/composites/composites.test.ts +252 -0
  200. package/src/composites/cron-job.ts +124 -0
  201. package/src/composites/gvc-environment.ts +104 -0
  202. package/src/composites/index.ts +17 -0
  203. package/src/composites/public-domain.ts +144 -0
  204. package/src/composites/secret-access.ts +136 -0
  205. package/src/composites/serverless-service.ts +143 -0
  206. package/src/composites/stateful-service.ts +179 -0
  207. package/src/coverage.ts +149 -0
  208. package/src/describe-resources.test.ts +252 -0
  209. package/src/describe-resources.ts +310 -0
  210. package/src/detect.ts +58 -0
  211. package/src/entity-props.ts +98 -0
  212. package/src/generated/index.d.ts +1649 -0
  213. package/src/generated/index.ts +156 -0
  214. package/src/generated/lexicon-cpln.json +2110 -0
  215. package/src/generated/runtime.ts +4 -0
  216. package/src/index.ts +31 -0
  217. package/src/init-templates.ts +131 -0
  218. package/src/kinds.ts +173 -0
  219. package/src/lint/audit-catalog.ts +230 -0
  220. package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
  221. package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
  222. package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
  223. package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
  224. package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
  225. package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
  226. package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
  227. package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
  228. package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
  229. package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
  230. package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
  231. package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
  232. package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
  233. package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
  234. package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
  235. package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
  236. package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
  237. package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
  238. package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
  239. package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
  240. package/src/lint/post-synth/helpers.ts +114 -0
  241. package/src/lint/post-synth/index.ts +45 -0
  242. package/src/lint/post-synth/post-synth.test.ts +737 -0
  243. package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
  244. package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
  245. package/src/lint/rules/index.ts +9 -0
  246. package/src/lint/rules/rules.test.ts +108 -0
  247. package/src/lsp/completions.test.ts +44 -0
  248. package/src/lsp/completions.ts +27 -0
  249. package/src/lsp/hover.test.ts +53 -0
  250. package/src/lsp/hover.ts +130 -0
  251. package/src/ownership.ts +28 -0
  252. package/src/package-cli.ts +25 -0
  253. package/src/plugin.test.ts +131 -0
  254. package/src/plugin.ts +187 -0
  255. package/src/reference-catalog.ts +110 -0
  256. package/src/serializer.test.ts +233 -0
  257. package/src/serializer.ts +253 -0
  258. package/src/skills/chant-cpln-secrets.md +110 -0
  259. package/src/skills/chant-cpln-workloads.md +78 -0
  260. package/src/skills/chant-cpln.md +103 -0
  261. package/src/spec/cpln-openapi.snapshot.json +10651 -0
  262. package/src/spec/fetch.ts +77 -0
  263. package/src/spec/parse.ts +451 -0
  264. package/src/spec/snapshot-cli.ts +71 -0
  265. package/src/validate-cli.ts +8 -0
  266. package/src/validate.ts +102 -0
@@ -0,0 +1,3 @@
1
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
2
+ export declare const cplnPlugin: LexiconPlugin;
3
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAId,MAAM,0BAA0B,CAAC;AAsClC,eAAO,MAAM,UAAU,EAAE,aA+IxB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * How observed cpln resources reference each other, so `chant graph --live` can
3
+ * rebuild the topology from a bag of nodes.
4
+ *
5
+ * Control Plane addresses resources by **link string**
6
+ * (`//gvc/prod/identity/api`), and edge reconstruction matches identifiers
7
+ * exactly, so a raw link never equals the `name` it points at. Rather than add
8
+ * an enrichment pass, `describeResources` resolves each link down to the bare
9
+ * name it ends in and files it under a stable attribute — `refs.identity`,
10
+ * `refs.gvc`, `refs.pullSecrets` — and the rules below read those. See
11
+ * {@link referenceAttributes} in `describe-resources.ts`.
12
+ *
13
+ * Names are unique per kind within an org (per GVC for the GVC-scoped kinds),
14
+ * so `name` is the identity for every kind and `targetKind` disambiguates the
15
+ * cases where two kinds could hold the same name.
16
+ *
17
+ * Containment vs reference matters for the picture: "this workload is in this
18
+ * GVC" is a boundary the renderer draws as a box, not a line from every
19
+ * resource to its GVC. It still declares `viaAttr` so a traversal can walk it —
20
+ * "which GVCs have no workloads" is a containment question.
21
+ */
22
+ import type { ReferenceCatalog } from "@intentius/chant/lexicon";
23
+ export declare const cplnReferenceCatalog: ReferenceCatalog;
24
+ //# sourceMappingURL=reference-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE,eAAO,MAAM,oBAAoB,EAAE,gBAkFlC,CAAC"}
@@ -0,0 +1,83 @@
1
+ import ts from "typescript";
2
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
3
+
4
+ /**
5
+ * CPL001: no literal credential material in a cpln declaration.
6
+ *
7
+ * This is a source-level rule rather than a post-synth check on purpose. By
8
+ * the time the manifest exists, a literal is just a string in a `data` block
9
+ * and there is nothing left to point at; here there is a file and a line, which
10
+ * is what makes the finding actionable — and, more to the point, this is the
11
+ * form the credential is in when it enters git history.
12
+ *
13
+ * `Secret` is the obvious carrier, but a credential pasted into a workload's
14
+ * env or an identity's cloud config is the same leak, so the rule fires
15
+ * anywhere inside a `new <cpln kind>(…)`.
16
+ *
17
+ * Complementary to CPL012, which catches the shape this cannot see — a
18
+ * plausible-looking value in a credential-named env var — from the model.
19
+ */
20
+
21
+ const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
22
+
23
+ const CREDENTIAL_SHAPES: Array<{ pattern: RegExp; label: string }> = [
24
+ { pattern: /^-----BEGIN [A-Z ]*PRIVATE KEY-----/, label: "private key material" },
25
+ { pattern: /^AKIA[0-9A-Z]{16}$/, label: "AWS access key id" },
26
+ { pattern: /^(ghp|gho|ghs|ghu)_[A-Za-z0-9]{20,}$/, label: "GitHub token" },
27
+ { pattern: /^github_pat_[A-Za-z0-9_]{20,}$/, label: "GitHub fine-grained token" },
28
+ { pattern: /^sk-[A-Za-z0-9_-]{20,}$/, label: "secret API key (sk-)" },
29
+ { pattern: /^xox[baprs]-[A-Za-z0-9-]{10,}$/, label: "Slack token" },
30
+ { pattern: /^ya29\.[A-Za-z0-9_-]{20,}$/, label: "Google OAuth access token" },
31
+ { pattern: /^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/, label: "JWT" },
32
+ {
33
+ pattern: /^postgres(ql)?:\/\/[^:@/\s]+:[^@/\s]+@/,
34
+ label: "database URL with an inline password",
35
+ },
36
+ { pattern: /^mysql:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
37
+ { pattern: /^mongodb(\+srv)?:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
38
+ ];
39
+
40
+ export const noSecretLiteralsRule: LintRule = {
41
+ id: "CPL001",
42
+ severity: "error",
43
+ category: "security",
44
+ description: "No literal credential material in cpln declarations — reference a secret instead",
45
+
46
+ check(context: LintContext): LintDiagnostic[] {
47
+ const diagnostics: LintDiagnostic[] = [];
48
+ const source = context.sourceFile;
49
+
50
+ const visit = (node: ts.Node, insideCplnNew: boolean): void => {
51
+ let inside = insideCplnNew;
52
+
53
+ if (ts.isNewExpression(node)) {
54
+ const name = node.expression.getText(source);
55
+ const short = name.split(".").pop() ?? name;
56
+ if (CPLN_KINDS.has(short)) inside = true;
57
+ }
58
+
59
+ if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
60
+ for (const { pattern, label } of CREDENTIAL_SHAPES) {
61
+ if (!pattern.test(node.text)) continue;
62
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
63
+ diagnostics.push({
64
+ ruleId: "CPL001",
65
+ severity: "error",
66
+ message:
67
+ `Literal ${label} in a cpln declaration. Store it as a Secret and reference it as ` +
68
+ `\`cpln://secret/NAME.FIELD\` — a literal here is a credential in git history.`,
69
+ file: context.filePath,
70
+ line: line + 1,
71
+ column: character + 1,
72
+ });
73
+ break;
74
+ }
75
+ }
76
+
77
+ ts.forEachChild(node, (child) => visit(child, inside));
78
+ };
79
+
80
+ visit(source, false);
81
+ return diagnostics;
82
+ },
83
+ };
@@ -0,0 +1,71 @@
1
+ import ts from "typescript";
2
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
3
+
4
+ /**
5
+ * CPL002: prefer a resource reference over a hand-written link.
6
+ *
7
+ * The cpln serializer resolves a declared resource passed where a link is
8
+ * expected into the correct link for its kind — including the GVC qualifier
9
+ * that the identity form needs and that is silently ignored when missing. A
10
+ * hand-spelled `"//gvc/prod/identity/api"` gets none of that: it does not
11
+ * follow a rename, it is not checked against anything at author time, and
12
+ * getting the shape subtly wrong fails at runtime rather than at apply.
13
+ *
14
+ * This is a style rule, not a correctness one — the literal form is valid, and
15
+ * is the only option for a resource this stack does not declare. It fires on
16
+ * the link syntax specifically, so ordinary strings are untouched.
17
+ *
18
+ * Not flagged: `cpln://secret/…` and `cpln://volumeset/…`. Those are runtime
19
+ * *resolution* URIs read by the container, not links between resources, and
20
+ * they have no reference form to prefer.
21
+ */
22
+
23
+ const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
24
+
25
+ /** `//gvc/prod/identity/api` or `//secret/db-password`. */
26
+ const LINK = /^\/\/(?:gvc\/[^/\s]+\/)?(gvc|workload|identity|volumeset|secret|policy|domain|ipset)\/[^/\s]+$/;
27
+
28
+ export const preferResourceReferenceRule: LintRule = {
29
+ id: "CPL002",
30
+ severity: "info",
31
+ category: "style",
32
+ description: "Prefer passing a declared resource over a hand-written Control Plane link",
33
+
34
+ check(context: LintContext): LintDiagnostic[] {
35
+ const diagnostics: LintDiagnostic[] = [];
36
+ const source = context.sourceFile;
37
+
38
+ const visit = (node: ts.Node, insideCplnNew: boolean): void => {
39
+ let inside = insideCplnNew;
40
+
41
+ if (ts.isNewExpression(node)) {
42
+ const name = node.expression.getText(source);
43
+ const short = name.split(".").pop() ?? name;
44
+ if (CPLN_KINDS.has(short)) inside = true;
45
+ }
46
+
47
+ if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
48
+ const match = LINK.exec(node.text);
49
+ if (match) {
50
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
51
+ diagnostics.push({
52
+ ruleId: "CPL002",
53
+ severity: "info",
54
+ message:
55
+ `Hand-written link "${node.text}". If the ${match[1]} is declared in this stack, pass the ` +
56
+ `resource itself — the serializer emits the correct link, including the GVC qualifier, and ` +
57
+ `the reference follows a rename.`,
58
+ file: context.filePath,
59
+ line: line + 1,
60
+ column: character + 1,
61
+ });
62
+ }
63
+ }
64
+
65
+ ts.forEachChild(node, (child) => visit(child, inside));
66
+ };
67
+
68
+ visit(source, false);
69
+ return diagnostics;
70
+ },
71
+ };
@@ -0,0 +1,41 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ const ANY_ADDRESS = "0.0.0.0/0";
6
+
7
+ /**
8
+ * CPL010: a workload that may call out to the entire internet.
9
+ *
10
+ * Inbound `0.0.0.0/0` is how a public service is *supposed* to be written, so
11
+ * it is not flagged. Outbound `0.0.0.0/0` is different: it is rarely required,
12
+ * it is the egress path for anything that gets a foothold in the container,
13
+ * and Control Plane supports hostname rules (with wildcards) precisely so it
14
+ * does not have to be all-or-nothing.
15
+ */
16
+ export const unrestrictedOutboundCheck: PostSynthCheck = {
17
+ id: "CPL010",
18
+ description: "Workloads should not allow outbound traffic to every address",
19
+
20
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
21
+ const diagnostics: PostSynthDiagnostic[] = [];
22
+
23
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
24
+ const outbound = readArray(entity, "spec", "firewallConfig", "external", "outboundAllowCIDR");
25
+ if (!outbound.includes(ANY_ADDRESS)) continue;
26
+
27
+ diagnostics.push({
28
+ checkId: "CPL010",
29
+ severity: "warning",
30
+ message:
31
+ `Workload "${name}" allows outbound traffic to ${ANY_ADDRESS}. Narrow it to the CIDRs it ` +
32
+ `actually calls, or use \`outboundAllowHostname\` (wildcards like "*.amazonaws.com" are ` +
33
+ `supported) so egress is bounded.`,
34
+ entity: name,
35
+ lexicon: "cpln",
36
+ });
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,38 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readString } from "../../entity-props";
3
+ import { GVC, WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /**
6
+ * CPL011: internal firewall opened to the whole org.
7
+ *
8
+ * `same-org` lets every workload in every GVC reach this one, which crosses the
9
+ * boundary a GVC exists to draw. `same-gvc` and `workload-list` cover almost
10
+ * every real case, and `workload-list` works cross-GVC when that is genuinely
11
+ * needed.
12
+ */
13
+ export const internalFirewallScopeCheck: PostSynthCheck = {
14
+ id: "CPL011",
15
+ description: "Internal firewall should not be opened to the entire org",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
21
+ const allowType = readString(entity, "spec", "firewallConfig", "internal", "inboundAllowType");
22
+ if (allowType !== "same-org") continue;
23
+
24
+ diagnostics.push({
25
+ checkId: "CPL011",
26
+ severity: "warning",
27
+ message:
28
+ `Workload "${name}" sets internal inboundAllowType "same-org", so every workload in the org — ` +
29
+ `including other GVCs — can reach it. Prefer "same-gvc", or "workload-list" with the specific ` +
30
+ `workloads (which also works across GVCs).`,
31
+ entity: name,
32
+ lexicon: "cpln",
33
+ });
34
+ }
35
+
36
+ return diagnostics;
37
+ },
38
+ };
@@ -0,0 +1,58 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
6
+ const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
7
+
8
+ /** Env var names that read as credentials. */
9
+ const CREDENTIAL_NAME = /(password|passwd|secret|token|apikey|api_key|access_key|private_key|credential)/i;
10
+
11
+ /**
12
+ * Values that look like a deliberate non-secret rather than a leaked one: an
13
+ * empty string, an obvious placeholder, or another `cpln://` reference.
14
+ */
15
+ const PLACEHOLDER = /^(|changeme|placeholder|todo|xxx+|\*+|<[^>]*>|\$\{[^}]*\})$/i;
16
+
17
+ /**
18
+ * CPL012: a credential-shaped env var carrying a literal value.
19
+ *
20
+ * The alternative is one string change — `cpln://secret/db.password` — so a
21
+ * literal here is almost always an oversight rather than a decision. Env values
22
+ * end up in the workload spec, which is readable by anyone with `get` on the
23
+ * workload, and in every diff of the manifest.
24
+ */
25
+ export const noLiteralCredentialEnvCheck: PostSynthCheck = {
26
+ id: "CPL012",
27
+ description: "Credential-shaped environment variables must reference a secret, not a literal",
28
+
29
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
30
+ const diagnostics: PostSynthDiagnostic[] = [];
31
+
32
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
33
+ for (const container of readArray(entity, "spec", "containers")) {
34
+ const containerName = readString(container, "name") ?? "?";
35
+ for (const env of readArray(container, "env")) {
36
+ const envName = readString(env, "name");
37
+ const value = readString(env, "value");
38
+ if (!envName || value === undefined) continue;
39
+ if (!CREDENTIAL_NAME.test(envName)) continue;
40
+ if (value.startsWith("cpln://") || PLACEHOLDER.test(value)) continue;
41
+
42
+ diagnostics.push({
43
+ checkId: "CPL012",
44
+ severity: "error",
45
+ message:
46
+ `Workload "${name}" container "${containerName}" sets env "${envName}" to a literal value. ` +
47
+ `Reference a secret instead — \`cpln://secret/NAME.FIELD\` — so the value is not stored in ` +
48
+ `the workload spec or its diffs.`,
49
+ entity: name,
50
+ lexicon: "cpln",
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,45 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { propsOf, readArray } from "../../entity-props";
3
+ import { GVC, POLICY, entitiesOfType } from "./helpers";
4
+
5
+ /**
6
+ * CPL013: an identity principal link that Control Plane will ignore.
7
+ *
8
+ * `//identity/NAME` is accepted by the policy API and then has no effect. Only
9
+ * the GVC-qualified `//gvc/GVC/identity/NAME` grants anything. Nothing reports
10
+ * this — the policy exists, the binding exists, and the permission is simply
11
+ * never granted.
12
+ */
13
+ export const qualifiedIdentityPrincipalCheck: PostSynthCheck = {
14
+ id: "CPL013",
15
+ description: "Policy bindings must name identities by their GVC-qualified link",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of entitiesOfType(ctx.entities, POLICY)) {
21
+ for (const binding of readArray(entity, "bindings")) {
22
+ const links = propsOf(binding).principalLinks;
23
+ if (!Array.isArray(links)) continue;
24
+
25
+ for (const link of links) {
26
+ if (typeof link !== "string") continue;
27
+ if (!/^\/\/identity\/[^/]+$/.test(link)) continue;
28
+
29
+ diagnostics.push({
30
+ checkId: "CPL013",
31
+ severity: "error",
32
+ message:
33
+ `Policy "${name}" binds the principal "${link}". Control Plane silently ignores the bare ` +
34
+ `//identity/NAME form — use //gvc/<gvc>/identity/${link.slice("//identity/".length)} instead. ` +
35
+ `The policy will apply cleanly and grant nothing as written.`,
36
+ entity: name,
37
+ lexicon: "cpln",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,54 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
6
+ const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
7
+
8
+ /**
9
+ * CPL014: a secret reference with no field qualifier.
10
+ *
11
+ * `cpln://secret/db` resolves to nothing for every secret type except `gcp`,
12
+ * which is conventionally mounted as a whole JSON file. The field name is not
13
+ * optional: `opaque` needs `.payload`, `userpass` needs `.username`/`.password`,
14
+ * `tls` needs `.cert`/`.key`, and a `dictionary` needs one of its keys.
15
+ *
16
+ * Volume mounts are exempt — mounting a secret as a directory is the documented
17
+ * way to consume a `dictionary` or a `gcp` credential file.
18
+ */
19
+ export const qualifiedSecretRefCheck: PostSynthCheck = {
20
+ id: "CPL014",
21
+ description: "Secret references in environment variables must name a field",
22
+
23
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
24
+ const diagnostics: PostSynthDiagnostic[] = [];
25
+
26
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
27
+ for (const container of readArray(entity, "spec", "containers")) {
28
+ const containerName = readString(container, "name") ?? "?";
29
+ for (const env of readArray(container, "env")) {
30
+ const envName = readString(env, "name") ?? "?";
31
+ const value = readString(env, "value");
32
+ if (!value) continue;
33
+
34
+ const match = SECRET_REF.exec(value);
35
+ if (!match || match[2]) continue;
36
+
37
+ diagnostics.push({
38
+ checkId: "CPL014",
39
+ severity: "error",
40
+ message:
41
+ `Workload "${name}" container "${containerName}" sets env "${envName}" to "${value}", which ` +
42
+ `names no field. Use \`cpln://secret/${match[1]}.FIELD\` — \`.payload\` for opaque, ` +
43
+ `\`.username\`/\`.password\` for userpass, \`.cert\`/\`.key\` for tls, a key name for a ` +
44
+ `dictionary. An unqualified reference resolves to nothing at runtime.`,
45
+ entity: name,
46
+ lexicon: "cpln",
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,58 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
4
+
5
+ /**
6
+ * CPL020: a serverless workload must expose exactly one HTTP port.
7
+ *
8
+ * Not "at least one" and not "any number" — exactly one, and it must be HTTP.
9
+ * Zero is the more common mistake and the more confusing one, because the
10
+ * workload deploys and reports healthy while serving nothing.
11
+ */
12
+ export const serverlessSinglePortCheck: PostSynthCheck = {
13
+ id: "CPL020",
14
+ description: "Serverless workloads must expose exactly one HTTP port",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
20
+ if (workloadType(entity) !== "serverless") continue;
21
+
22
+ const ports = exposedPorts(entity);
23
+ if (ports.length !== 1) {
24
+ diagnostics.push({
25
+ checkId: "CPL020",
26
+ severity: "error",
27
+ message:
28
+ `Serverless workload "${name}" exposes ${ports.length} ports; exactly one HTTP port is required. ` +
29
+ (ports.length === 0
30
+ ? `It will deploy and serve nothing.`
31
+ : `Use a standard workload if it genuinely needs several.`),
32
+ entity: name,
33
+ lexicon: "cpln",
34
+ });
35
+ continue;
36
+ }
37
+
38
+ // The single port must be HTTP. `tcp` and `grpc` need a standard workload.
39
+ for (const container of readArray(entity, "spec", "containers")) {
40
+ for (const port of readArray(container, "ports")) {
41
+ const protocol = readString(port, "protocol") ?? "http";
42
+ if (protocol === "http" || protocol === "http2") continue;
43
+ diagnostics.push({
44
+ checkId: "CPL020",
45
+ severity: "error",
46
+ message:
47
+ `Serverless workload "${name}" exposes a "${protocol}" port. Serverless supports only http ` +
48
+ `and http2 — use a standard workload for ${protocol}.`,
49
+ entity: name,
50
+ lexicon: "cpln",
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,64 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readPath, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
4
+
5
+ /**
6
+ * CPL021: a cron workload needs a schedule and must expose no ports.
7
+ *
8
+ * Cron cannot serve traffic, so a port on one is always a misunderstanding —
9
+ * usually a workload converted from serverless with the ports left behind. A
10
+ * missing `spec.job.schedule` is the other half: `spec.job` is *required* for
11
+ * cron and forbidden for every other type.
12
+ */
13
+ export const cronJobShapeCheck: PostSynthCheck = {
14
+ id: "CPL021",
15
+ description: "Cron workloads must declare a schedule and expose no ports",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
21
+ const type = workloadType(entity);
22
+ const job = readPath(entity, "spec", "job");
23
+ const schedule = readString(entity, "spec", "job", "schedule");
24
+
25
+ if (type === "cron") {
26
+ if (!schedule) {
27
+ diagnostics.push({
28
+ checkId: "CPL021",
29
+ severity: "error",
30
+ message: `Cron workload "${name}" has no spec.job.schedule. A cron workload requires one.`,
31
+ entity: name,
32
+ lexicon: "cpln",
33
+ });
34
+ }
35
+
36
+ const ports = exposedPorts(entity);
37
+ if (ports.length > 0) {
38
+ diagnostics.push({
39
+ checkId: "CPL021",
40
+ severity: "error",
41
+ message:
42
+ `Cron workload "${name}" exposes ${ports.length} port(s) (${ports.join(", ")}). Cron workloads ` +
43
+ `cannot serve traffic — remove the ports, or use a standard workload.`,
44
+ entity: name,
45
+ lexicon: "cpln",
46
+ });
47
+ }
48
+ continue;
49
+ }
50
+
51
+ if (job !== undefined) {
52
+ diagnostics.push({
53
+ checkId: "CPL021",
54
+ severity: "error",
55
+ message: `Workload "${name}" is type "${type}" but sets spec.job. Only cron workloads may declare one.`,
56
+ entity: name,
57
+ lexicon: "cpln",
58
+ });
59
+ }
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };
@@ -0,0 +1,64 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readNumber, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /**
6
+ * CPL022: `port` and `ports` are mutually exclusive, and port numbers must be
7
+ * unique across every container in a workload.
8
+ *
9
+ * The duplicate case is the one worth catching: two containers each binding
10
+ * 8080 is accepted at author time, rejected at apply, and reads as correct.
11
+ */
12
+ export const containerPortsCheck: PostSynthCheck = {
13
+ id: "CPL022",
14
+ description: "Container ports must not mix `port` and `ports`, and must be unique across containers",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
20
+ const seen = new Map<number, string>();
21
+
22
+ for (const container of readArray(entity, "spec", "containers")) {
23
+ const containerName = readString(container, "name") ?? "?";
24
+ const single = readNumber(container, "port");
25
+ const list = readArray(container, "ports");
26
+
27
+ if (single !== undefined && list.length > 0) {
28
+ diagnostics.push({
29
+ checkId: "CPL022",
30
+ severity: "error",
31
+ message:
32
+ `Workload "${name}" container "${containerName}" sets both \`port\` and \`ports\`. ` +
33
+ `They are mutually exclusive — keep \`ports\`.`,
34
+ entity: name,
35
+ lexicon: "cpln",
36
+ });
37
+ }
38
+
39
+ const numbers = [
40
+ ...(single !== undefined ? [single] : []),
41
+ ...list.map((port) => readNumber(port, "number")).filter((n): n is number => n !== undefined),
42
+ ];
43
+
44
+ for (const number of numbers) {
45
+ const owner = seen.get(number);
46
+ if (owner !== undefined && owner !== containerName) {
47
+ diagnostics.push({
48
+ checkId: "CPL022",
49
+ severity: "error",
50
+ message:
51
+ `Workload "${name}" exposes port ${number} from both "${owner}" and "${containerName}". ` +
52
+ `Port numbers must be unique across the containers of one workload.`,
53
+ entity: name,
54
+ lexicon: "cpln",
55
+ });
56
+ }
57
+ seen.set(number, containerName);
58
+ }
59
+ }
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };