@intentius/chant-lexicon-gcp 0.0.18 → 0.0.24

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 (68) hide show
  1. package/dist/integrity.json +12 -8
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +18141 -0
  4. package/dist/rules/schema-registry.ts +91 -0
  5. package/dist/rules/wgc101.ts +1 -1
  6. package/dist/rules/wgc401.ts +59 -0
  7. package/dist/rules/wgc402.ts +54 -0
  8. package/dist/rules/wgc403.ts +84 -0
  9. package/dist/skills/{chant-gke.md → chant-gcp-gke.md} +1 -1
  10. package/dist/skills/chant-gcp-patterns.md +3 -2
  11. package/dist/skills/chant-gcp-security.md +3 -2
  12. package/dist/skills/chant-gcp.md +363 -28
  13. package/package.json +20 -2
  14. package/src/codegen/docs.test.ts +16 -0
  15. package/src/codegen/generate.test.ts +18 -0
  16. package/src/codegen/generate.ts +11 -0
  17. package/src/codegen/package.test.ts +16 -0
  18. package/src/composites/cloud-function.ts +23 -15
  19. package/src/composites/cloud-run-service.ts +20 -13
  20. package/src/composites/cloud-sql-instance.ts +18 -14
  21. package/src/composites/composites.test.ts +94 -62
  22. package/src/composites/gcs-bucket.ts +13 -9
  23. package/src/composites/gke-cluster.ts +91 -16
  24. package/src/composites/index.ts +11 -11
  25. package/src/composites/managed-certificate.ts +19 -15
  26. package/src/composites/private-service.ts +23 -15
  27. package/src/composites/pubsub-pipeline.ts +30 -18
  28. package/src/composites/secure-project.ts +42 -27
  29. package/src/composites/vpc-network.ts +42 -35
  30. package/src/generated/lexicon-gcp.json +18141 -0
  31. package/src/import/import-fixtures.test.ts +98 -0
  32. package/src/index.ts +11 -11
  33. package/src/lint/post-synth/gcp-helpers.test.ts +166 -0
  34. package/src/lint/post-synth/post-synth.test.ts +132 -1
  35. package/src/lint/post-synth/schema-registry.ts +91 -0
  36. package/src/lint/post-synth/wgc101.test.ts +40 -0
  37. package/src/lint/post-synth/wgc101.ts +1 -1
  38. package/src/lint/post-synth/wgc102.test.ts +38 -0
  39. package/src/lint/post-synth/wgc103.test.ts +38 -0
  40. package/src/lint/post-synth/wgc104.test.ts +37 -0
  41. package/src/lint/post-synth/wgc105.test.ts +46 -0
  42. package/src/lint/post-synth/wgc106.test.ts +38 -0
  43. package/src/lint/post-synth/wgc107.test.ts +38 -0
  44. package/src/lint/post-synth/wgc108.test.ts +42 -0
  45. package/src/lint/post-synth/wgc109.test.ts +46 -0
  46. package/src/lint/post-synth/wgc110.test.ts +37 -0
  47. package/src/lint/post-synth/wgc111.test.ts +46 -0
  48. package/src/lint/post-synth/wgc112.test.ts +48 -0
  49. package/src/lint/post-synth/wgc113.test.ts +36 -0
  50. package/src/lint/post-synth/wgc201.test.ts +38 -0
  51. package/src/lint/post-synth/wgc202.test.ts +38 -0
  52. package/src/lint/post-synth/wgc203.test.ts +45 -0
  53. package/src/lint/post-synth/wgc204.test.ts +42 -0
  54. package/src/lint/post-synth/wgc301.test.ts +39 -0
  55. package/src/lint/post-synth/wgc302.test.ts +36 -0
  56. package/src/lint/post-synth/wgc303.test.ts +37 -0
  57. package/src/lint/post-synth/wgc401.test.ts +46 -0
  58. package/src/lint/post-synth/wgc401.ts +59 -0
  59. package/src/lint/post-synth/wgc402.test.ts +40 -0
  60. package/src/lint/post-synth/wgc402.ts +54 -0
  61. package/src/lint/post-synth/wgc403.test.ts +59 -0
  62. package/src/lint/post-synth/wgc403.ts +84 -0
  63. package/src/plugin.test.ts +4 -1
  64. package/src/plugin.ts +258 -177
  65. package/src/skills/{chant-gke.md → chant-gcp-gke.md} +1 -1
  66. package/src/skills/chant-gcp-patterns.md +3 -2
  67. package/src/skills/chant-gcp-security.md +3 -2
  68. package/src/skills/chant-gcp.md +363 -28
@@ -0,0 +1,98 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { GcpParser } from "./parser";
3
+ import { GcpGenerator } from "./generator";
4
+
5
+ const parser = new GcpParser();
6
+
7
+ describe("GCP import with inline YAML fixtures", () => {
8
+ test("parses a single StorageBucket manifest", () => {
9
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
10
+ kind: StorageBucket
11
+ metadata:
12
+ name: my-bucket
13
+ spec:
14
+ location: US
15
+ uniformBucketLevelAccess: true
16
+ `;
17
+ const ir = parser.parse(yaml);
18
+ expect(ir.resources).toHaveLength(1);
19
+ expect(ir.resources[0].logicalName).toBe("my-bucket");
20
+ expect(ir.resources[0].type).toContain("Bucket");
21
+ });
22
+
23
+ test("parses multi-document YAML into multiple resources", () => {
24
+ const yaml = `apiVersion: compute.cnrm.cloud.google.com/v1beta1
25
+ kind: ComputeNetwork
26
+ metadata:
27
+ name: my-network
28
+ spec:
29
+ autoCreateSubnetworks: false
30
+ ---
31
+ apiVersion: compute.cnrm.cloud.google.com/v1beta1
32
+ kind: ComputeSubnetwork
33
+ metadata:
34
+ name: my-subnet
35
+ spec:
36
+ region: us-central1
37
+ ipCidrRange: "10.0.0.0/24"
38
+ networkRef:
39
+ name: my-network
40
+ `;
41
+ const ir = parser.parse(yaml);
42
+ expect(ir.resources).toHaveLength(2);
43
+ expect(ir.resources[0].logicalName).toBe("my-network");
44
+ expect(ir.resources[1].logicalName).toBe("my-subnet");
45
+ });
46
+
47
+ test("skips non-Config-Connector resources", () => {
48
+ const yaml = `apiVersion: apps/v1
49
+ kind: Deployment
50
+ metadata:
51
+ name: my-deploy
52
+ spec:
53
+ replicas: 3
54
+ ---
55
+ apiVersion: storage.cnrm.cloud.google.com/v1beta1
56
+ kind: StorageBucket
57
+ metadata:
58
+ name: my-bucket
59
+ spec:
60
+ location: US
61
+ `;
62
+ const ir = parser.parse(yaml);
63
+ expect(ir.resources).toHaveLength(1);
64
+ expect(ir.resources[0].type).toContain("Bucket");
65
+ });
66
+
67
+ test("preserves spec properties in parsed IR", () => {
68
+ const yaml = `apiVersion: sql.cnrm.cloud.google.com/v1beta1
69
+ kind: SQLInstance
70
+ metadata:
71
+ name: my-db
72
+ spec:
73
+ databaseVersion: POSTGRES_15
74
+ settings:
75
+ tier: db-f1-micro
76
+ backupConfiguration:
77
+ enabled: true
78
+ `;
79
+ const ir = parser.parse(yaml);
80
+ expect(ir.resources).toHaveLength(1);
81
+ expect(ir.resources[0].properties.databaseVersion).toBe("POSTGRES_15");
82
+ });
83
+
84
+ test("round-trips through parser and generator", () => {
85
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
86
+ kind: StorageBucket
87
+ metadata:
88
+ name: my-bucket
89
+ spec:
90
+ location: US
91
+ `;
92
+ const ir = parser.parse(yaml);
93
+ const generator = new GcpGenerator();
94
+ const ts = generator.generate(ir);
95
+ expect(ts).toContain("Bucket");
96
+ expect(ts).toContain("import");
97
+ });
98
+ });
package/src/index.ts CHANGED
@@ -20,20 +20,20 @@ export * from "./generated/index";
20
20
 
21
21
  // Composites
22
22
  export {
23
- GkeCluster, CloudRunService, CloudSqlInstance, GcsBucket, VpcNetwork,
23
+ GkeCluster, CloudRunServiceComposite, CloudSqlInstance, GcsBucket, VpcNetwork,
24
24
  PubSubPipeline, CloudFunctionWithTrigger, PrivateService, ManagedCertificate, SecureProject,
25
25
  } from "./composites/index";
26
26
  export type {
27
- GkeClusterProps, GkeClusterResult,
28
- CloudRunServiceProps, CloudRunServiceResult,
29
- CloudSqlInstanceProps, CloudSqlInstanceResult,
30
- GcsBucketProps, GcsBucketResult,
31
- VpcNetworkProps, VpcNetworkResult, VpcSubnet,
32
- PubSubPipelineProps, PubSubPipelineResult,
33
- CloudFunctionWithTriggerProps, CloudFunctionWithTriggerResult,
34
- PrivateServiceProps, PrivateServiceResult,
35
- ManagedCertificateProps, ManagedCertificateResult,
36
- SecureProjectProps, SecureProjectResult,
27
+ GkeClusterProps,
28
+ CloudRunServiceProps,
29
+ CloudSqlInstanceProps,
30
+ GcsBucketProps,
31
+ VpcNetworkProps, VpcSubnet,
32
+ PubSubPipelineProps,
33
+ CloudFunctionWithTriggerProps,
34
+ PrivateServiceProps,
35
+ ManagedCertificateProps,
36
+ SecureProjectProps,
37
37
  } from "./composites/index";
38
38
 
39
39
  // IAM role constants
@@ -0,0 +1,166 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ parseGcpManifests,
4
+ isConfigConnectorResource,
5
+ getSpec,
6
+ getAnnotations,
7
+ getResourceName,
8
+ findResourceRefs,
9
+ } from "./gcp-helpers";
10
+
11
+ describe("parseGcpManifests", () => {
12
+ test("parses single document", () => {
13
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
14
+ kind: StorageBucket
15
+ metadata:
16
+ name: my-bucket
17
+ spec:
18
+ location: US
19
+ `;
20
+ const manifests = parseGcpManifests(yaml);
21
+ expect(manifests).toHaveLength(1);
22
+ expect(manifests[0].kind).toBe("StorageBucket");
23
+ expect(manifests[0].metadata?.name).toBe("my-bucket");
24
+ });
25
+
26
+ test("parses multi-document YAML", () => {
27
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
28
+ kind: StorageBucket
29
+ metadata:
30
+ name: bucket-a
31
+ spec:
32
+ location: US
33
+ ---
34
+ apiVersion: storage.cnrm.cloud.google.com/v1beta1
35
+ kind: StorageBucket
36
+ metadata:
37
+ name: bucket-b
38
+ spec:
39
+ location: EU
40
+ `;
41
+ const manifests = parseGcpManifests(yaml);
42
+ expect(manifests).toHaveLength(2);
43
+ expect(manifests[0].metadata?.name).toBe("bucket-a");
44
+ expect(manifests[1].metadata?.name).toBe("bucket-b");
45
+ });
46
+
47
+ test("skips empty documents", () => {
48
+ const yaml = `---
49
+ ---
50
+ apiVersion: storage.cnrm.cloud.google.com/v1beta1
51
+ kind: StorageBucket
52
+ metadata:
53
+ name: my-bucket
54
+ spec:
55
+ location: US
56
+ ---
57
+ `;
58
+ const manifests = parseGcpManifests(yaml);
59
+ expect(manifests).toHaveLength(1);
60
+ });
61
+
62
+ test("returns empty array for empty input", () => {
63
+ const manifests = parseGcpManifests("");
64
+ expect(manifests).toHaveLength(0);
65
+ });
66
+ });
67
+
68
+ describe("isConfigConnectorResource", () => {
69
+ test("returns true for cnrm resource", () => {
70
+ expect(
71
+ isConfigConnectorResource({
72
+ apiVersion: "storage.cnrm.cloud.google.com/v1beta1",
73
+ kind: "StorageBucket",
74
+ }),
75
+ ).toBe(true);
76
+ });
77
+
78
+ test("returns false for non-cnrm resource", () => {
79
+ expect(
80
+ isConfigConnectorResource({
81
+ apiVersion: "apps/v1",
82
+ kind: "Deployment",
83
+ }),
84
+ ).toBe(false);
85
+ });
86
+
87
+ test("returns false when apiVersion is missing", () => {
88
+ expect(isConfigConnectorResource({ kind: "StorageBucket" })).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe("getSpec", () => {
93
+ test("returns spec when present", () => {
94
+ const spec = getSpec({ spec: { location: "US" } });
95
+ expect(spec).toEqual({ location: "US" });
96
+ });
97
+
98
+ test("returns undefined when spec is missing", () => {
99
+ expect(getSpec({})).toBeUndefined();
100
+ });
101
+ });
102
+
103
+ describe("getAnnotations", () => {
104
+ test("returns annotations when present", () => {
105
+ const annotations = getAnnotations({
106
+ metadata: {
107
+ name: "x",
108
+ annotations: { "cnrm.cloud.google.com/project-id": "my-project" },
109
+ },
110
+ });
111
+ expect(annotations).toEqual({
112
+ "cnrm.cloud.google.com/project-id": "my-project",
113
+ });
114
+ });
115
+
116
+ test("returns undefined when annotations missing", () => {
117
+ expect(getAnnotations({ metadata: { name: "x" } })).toBeUndefined();
118
+ });
119
+
120
+ test("returns undefined when metadata missing", () => {
121
+ expect(getAnnotations({})).toBeUndefined();
122
+ });
123
+ });
124
+
125
+ describe("getResourceName", () => {
126
+ test("returns name from metadata", () => {
127
+ expect(getResourceName({ metadata: { name: "my-bucket" } })).toBe(
128
+ "my-bucket",
129
+ );
130
+ });
131
+
132
+ test("returns 'unknown' when name missing", () => {
133
+ expect(getResourceName({})).toBe("unknown");
134
+ });
135
+ });
136
+
137
+ describe("findResourceRefs", () => {
138
+ test("finds refs in spec", () => {
139
+ const refs = findResourceRefs({
140
+ clusterRef: { name: "my-cluster" },
141
+ networkRef: { name: "my-network" },
142
+ });
143
+ expect(refs.has("my-cluster")).toBe(true);
144
+ expect(refs.has("my-network")).toBe(true);
145
+ expect(refs.size).toBe(2);
146
+ });
147
+
148
+ test("skips external refs", () => {
149
+ const refs = findResourceRefs({
150
+ projectRef: { name: "my-project", external: true },
151
+ });
152
+ expect(refs.size).toBe(0);
153
+ });
154
+
155
+ test("finds refs nested in arrays", () => {
156
+ const refs = findResourceRefs({
157
+ items: [{ subnetRef: { name: "subnet-1" } }],
158
+ });
159
+ expect(refs.has("subnet-1")).toBe(true);
160
+ });
161
+
162
+ test("returns empty set for null/undefined", () => {
163
+ expect(findResourceRefs(null).size).toBe(0);
164
+ expect(findResourceRefs(undefined).size).toBe(0);
165
+ });
166
+ });
@@ -19,6 +19,9 @@ import { wgc303 } from "./wgc303";
19
19
  import { wgc111 } from "./wgc111";
20
20
  import { wgc112 } from "./wgc112";
21
21
  import { wgc113 } from "./wgc113";
22
+ import { wgc401 } from "./wgc401";
23
+ import { wgc402 } from "./wgc402";
24
+ import { wgc403 } from "./wgc403";
22
25
 
23
26
  function makeCtx(yaml: string) {
24
27
  return {
@@ -51,7 +54,8 @@ metadata:
51
54
  spec:
52
55
  location: US
53
56
  encryption:
54
- defaultKmsKeyName: projects/p/locations/l/keyRings/kr/cryptoKeys/k
57
+ kmsKeyRef:
58
+ external: projects/p/locations/l/keyRings/kr/cryptoKeys/k
55
59
  `;
56
60
  const diags = wgc101.check(makeCtx(yaml));
57
61
  const bucketDiags = diags.filter(d => d.entity === "my-bucket");
@@ -709,3 +713,130 @@ spec:
709
713
  expect(diags).toHaveLength(0);
710
714
  });
711
715
  });
716
+
717
+ // ── WGC401: Unknown spec field ─────────────────────────────────────
718
+
719
+ describe("WGC401: unknown spec field", () => {
720
+ test("flags unknown field with did-you-mean suggestion", () => {
721
+ const yaml = `apiVersion: compute.cnrm.cloud.google.com/v1beta1
722
+ kind: ComputeFirewall
723
+ metadata:
724
+ name: my-fw
725
+ spec:
726
+ allowed:
727
+ - protocol: tcp
728
+ networkRef:
729
+ name: my-network
730
+ `;
731
+ const diags = wgc401.check(makeCtx(yaml));
732
+ // "allowed" is not a valid field — "allow" is. Should flag it.
733
+ const unknownDiags = diags.filter(d => d.checkId === "WGC401");
734
+ // If schema is loaded, this will flag "allowed"
735
+ // If schema is not loaded (pre-generate), skip gracefully
736
+ if (unknownDiags.length > 0) {
737
+ expect(unknownDiags[0].severity).toBe("error");
738
+ expect(unknownDiags[0].message).toContain("allowed");
739
+ }
740
+ });
741
+
742
+ test("no diagnostic for valid fields", () => {
743
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
744
+ kind: StorageBucket
745
+ metadata:
746
+ name: my-bucket
747
+ spec:
748
+ location: US
749
+ `;
750
+ const diags = wgc401.check(makeCtx(yaml));
751
+ // "location" is a valid StorageBucket field
752
+ const unknownDiags = diags.filter(d => d.checkId === "WGC401");
753
+ expect(unknownDiags).toHaveLength(0);
754
+ });
755
+ });
756
+
757
+ // ── WGC402: Missing required field ─────────────────────────────────
758
+
759
+ describe("WGC402: missing required field", () => {
760
+ test("flags missing required field", () => {
761
+ const yaml = `apiVersion: compute.cnrm.cloud.google.com/v1beta1
762
+ kind: ComputeAddress
763
+ metadata:
764
+ name: my-addr
765
+ spec:
766
+ description: test
767
+ `;
768
+ const diags = wgc402.check(makeCtx(yaml));
769
+ const requiredDiags = diags.filter(d => d.checkId === "WGC402");
770
+ // ComputeAddress requires "location" — if schema is loaded, this flags it
771
+ if (requiredDiags.length > 0) {
772
+ expect(requiredDiags[0].severity).toBe("error");
773
+ expect(requiredDiags[0].message).toContain("required");
774
+ }
775
+ });
776
+
777
+ test("no diagnostic when all required fields present", () => {
778
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
779
+ kind: StorageBucket
780
+ metadata:
781
+ name: my-bucket
782
+ spec:
783
+ location: US
784
+ `;
785
+ const diags = wgc402.check(makeCtx(yaml));
786
+ const requiredDiags = diags.filter(d => d.checkId === "WGC402");
787
+ expect(requiredDiags).toHaveLength(0);
788
+ });
789
+ });
790
+
791
+ // ── WGC403: Type/structure mismatch ────────────────────────────────
792
+
793
+ describe("WGC403: type/structure mismatch", () => {
794
+ test("flags string where number expected", () => {
795
+ const yaml = `apiVersion: cloudfunctions.cnrm.cloud.google.com/v1beta1
796
+ kind: CloudFunctionsFunction
797
+ metadata:
798
+ name: my-fn
799
+ spec:
800
+ runtime: nodejs18
801
+ availableMemoryMb: "512"
802
+ region: us-central1
803
+ `;
804
+ const diags = wgc403.check(makeCtx(yaml));
805
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
806
+ if (typeDiags.length > 0) {
807
+ expect(typeDiags[0].severity).toBe("error");
808
+ expect(typeDiags[0].message).toContain("number");
809
+ expect(typeDiags[0].message).toContain("string");
810
+ }
811
+ });
812
+
813
+ test("flags bare string instead of resourceRef object", () => {
814
+ const yaml = `apiVersion: pubsub.cnrm.cloud.google.com/v1beta1
815
+ kind: PubSubSubscription
816
+ metadata:
817
+ name: my-sub
818
+ spec:
819
+ topicRef: my-topic
820
+ `;
821
+ const diags = wgc403.check(makeCtx(yaml));
822
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
823
+ if (typeDiags.length > 0) {
824
+ expect(typeDiags[0].severity).toBe("error");
825
+ expect(typeDiags[0].message).toContain("resourceRef");
826
+ }
827
+ });
828
+
829
+ test("no diagnostic with correct types", () => {
830
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
831
+ kind: StorageBucket
832
+ metadata:
833
+ name: my-bucket
834
+ spec:
835
+ location: US
836
+ uniformBucketLevelAccess: true
837
+ `;
838
+ const diags = wgc403.check(makeCtx(yaml));
839
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
840
+ expect(typeDiags).toHaveLength(0);
841
+ });
842
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Schema registry for GCP Config Connector resources.
3
+ *
4
+ * Loads lexicon-gcp.json and builds a lookup from CRD kind to field schema,
5
+ * used by post-synth rules to validate YAML against known CRD schemas.
6
+ */
7
+
8
+ import { createRequire } from "module";
9
+ const require = createRequire(import.meta.url);
10
+
11
+ export interface FieldSchema {
12
+ type: string;
13
+ required: boolean;
14
+ enum?: string[];
15
+ ref?: boolean;
16
+ }
17
+
18
+ export interface ResourceSchema {
19
+ fields: Record<string, FieldSchema>;
20
+ required: string[];
21
+ }
22
+
23
+ interface LexiconEntry {
24
+ kind: "resource" | "property";
25
+ gvkKind?: string;
26
+ schema?: ResourceSchema;
27
+ }
28
+
29
+ let cachedRegistry: Map<string, ResourceSchema> | null = null;
30
+
31
+ /**
32
+ * Get the schema registry: Map<gvkKind, ResourceSchema>.
33
+ */
34
+ export function getSchemaRegistry(): Map<string, ResourceSchema> {
35
+ if (cachedRegistry) return cachedRegistry;
36
+
37
+ cachedRegistry = new Map();
38
+ try {
39
+ const lexicon = require("../../generated/lexicon-gcp.json") as Record<string, LexiconEntry>;
40
+ for (const entry of Object.values(lexicon)) {
41
+ if (entry.kind === "resource" && entry.gvkKind && entry.schema) {
42
+ cachedRegistry.set(entry.gvkKind, entry.schema);
43
+ }
44
+ }
45
+ } catch {
46
+ // Lexicon JSON not yet generated — return empty registry
47
+ }
48
+
49
+ return cachedRegistry;
50
+ }
51
+
52
+ /**
53
+ * Levenshtein distance between two strings.
54
+ */
55
+ export function levenshtein(a: string, b: string): number {
56
+ const m = a.length;
57
+ const n = b.length;
58
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
59
+
60
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
61
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
62
+
63
+ for (let i = 1; i <= m; i++) {
64
+ for (let j = 1; j <= n; j++) {
65
+ dp[i][j] = a[i - 1] === b[j - 1]
66
+ ? dp[i - 1][j - 1]
67
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
68
+ }
69
+ }
70
+
71
+ return dp[m][n];
72
+ }
73
+
74
+ /**
75
+ * Find the closest field name by Levenshtein distance.
76
+ * Returns the suggestion if distance ≤ 3, otherwise undefined.
77
+ */
78
+ export function suggestField(unknown: string, knownFields: string[]): string | undefined {
79
+ let best: string | undefined;
80
+ let bestDist = 4; // threshold
81
+
82
+ for (const field of knownFields) {
83
+ const dist = levenshtein(unknown.toLowerCase(), field.toLowerCase());
84
+ if (dist < bestDist) {
85
+ bestDist = dist;
86
+ best = field;
87
+ }
88
+ }
89
+
90
+ return best;
91
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { wgc101 } from "./wgc101";
3
+
4
+ function makeCtx(yaml: string) {
5
+ return {
6
+ outputs: new Map([["gcp", yaml]]),
7
+ };
8
+ }
9
+
10
+ describe("WGC101: missing encryption", () => {
11
+ test("flags StorageBucket without encryption", () => {
12
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
13
+ kind: StorageBucket
14
+ metadata:
15
+ name: my-bucket
16
+ spec:
17
+ location: US
18
+ `;
19
+ const diags = wgc101.check(makeCtx(yaml));
20
+ expect(diags.length).toBeGreaterThanOrEqual(1);
21
+ expect(diags[0].checkId).toBe("WGC101");
22
+ expect(diags[0].severity).toBe("warning");
23
+ });
24
+
25
+ test("no diagnostic when encryption present", () => {
26
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
27
+ kind: StorageBucket
28
+ metadata:
29
+ name: my-bucket
30
+ spec:
31
+ location: US
32
+ encryption:
33
+ kmsKeyRef:
34
+ external: projects/p/locations/l/keyRings/kr/cryptoKeys/k
35
+ `;
36
+ const diags = wgc101.check(makeCtx(yaml));
37
+ const bucketDiags = diags.filter(d => d.entity === "my-bucket");
38
+ expect(bucketDiags).toHaveLength(0);
39
+ });
40
+ });
@@ -30,7 +30,7 @@ export const wgc101: PostSynthCheck = {
30
30
  diagnostics.push({
31
31
  checkId: "WGC101",
32
32
  severity: "warning",
33
- message: `StorageBucket "${name}" has no encryption configuration — consider adding spec.encryption.defaultKmsKeyName`,
33
+ message: `StorageBucket "${name}" has no encryption configuration — consider adding spec.encryption.kmsKeyRef`,
34
34
  entity: name,
35
35
  lexicon: "gcp",
36
36
  });
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { wgc102 } from "./wgc102";
3
+
4
+ function makeCtx(yaml: string) {
5
+ return {
6
+ outputs: new Map([["gcp", yaml]]),
7
+ };
8
+ }
9
+
10
+ describe("WGC102: public IAM in output", () => {
11
+ test("flags allUsers in output", () => {
12
+ const yaml = `apiVersion: iam.cnrm.cloud.google.com/v1beta1
13
+ kind: IAMPolicyMember
14
+ metadata:
15
+ name: public-access
16
+ spec:
17
+ member: allUsers
18
+ role: roles/run.invoker
19
+ `;
20
+ const diags = wgc102.check(makeCtx(yaml));
21
+ expect(diags.length).toBeGreaterThanOrEqual(1);
22
+ expect(diags[0].checkId).toBe("WGC102");
23
+ expect(diags[0].severity).toBe("warning");
24
+ });
25
+
26
+ test("no diagnostic when no public members", () => {
27
+ const yaml = `apiVersion: iam.cnrm.cloud.google.com/v1beta1
28
+ kind: IAMPolicyMember
29
+ metadata:
30
+ name: private-access
31
+ spec:
32
+ member: serviceAccount:sa@project.iam.gserviceaccount.com
33
+ role: roles/viewer
34
+ `;
35
+ const diags = wgc102.check(makeCtx(yaml));
36
+ expect(diags).toHaveLength(0);
37
+ });
38
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { wgc103 } from "./wgc103";
3
+
4
+ function makeCtx(yaml: string) {
5
+ return {
6
+ outputs: new Map([["gcp", yaml]]),
7
+ };
8
+ }
9
+
10
+ describe("WGC103: missing project annotation", () => {
11
+ test("flags resource without project annotation", () => {
12
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
13
+ kind: StorageBucket
14
+ metadata:
15
+ name: my-bucket
16
+ spec:
17
+ location: US
18
+ `;
19
+ const diags = wgc103.check(makeCtx(yaml));
20
+ expect(diags.length).toBeGreaterThanOrEqual(1);
21
+ expect(diags[0].checkId).toBe("WGC103");
22
+ expect(diags[0].severity).toBe("info");
23
+ });
24
+
25
+ test("no diagnostic when project annotation present", () => {
26
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
27
+ kind: StorageBucket
28
+ metadata:
29
+ name: my-bucket
30
+ annotations:
31
+ cnrm.cloud.google.com/project-id: my-project
32
+ spec:
33
+ location: US
34
+ `;
35
+ const diags = wgc103.check(makeCtx(yaml));
36
+ expect(diags).toHaveLength(0);
37
+ });
38
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { wgc104 } from "./wgc104";
3
+
4
+ function makeCtx(yaml: string) {
5
+ return {
6
+ outputs: new Map([["gcp", yaml]]),
7
+ };
8
+ }
9
+
10
+ describe("WGC104: missing uniform bucket access", () => {
11
+ test("flags StorageBucket without uniformBucketLevelAccess", () => {
12
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
13
+ kind: StorageBucket
14
+ metadata:
15
+ name: my-bucket
16
+ spec:
17
+ location: US
18
+ `;
19
+ const diags = wgc104.check(makeCtx(yaml));
20
+ expect(diags.length).toBeGreaterThanOrEqual(1);
21
+ expect(diags[0].checkId).toBe("WGC104");
22
+ expect(diags[0].severity).toBe("warning");
23
+ });
24
+
25
+ test("no diagnostic when uniformBucketLevelAccess is true", () => {
26
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
27
+ kind: StorageBucket
28
+ metadata:
29
+ name: my-bucket
30
+ spec:
31
+ location: US
32
+ uniformBucketLevelAccess: true
33
+ `;
34
+ const diags = wgc104.check(makeCtx(yaml));
35
+ expect(diags).toHaveLength(0);
36
+ });
37
+ });