@intentius/chant-lexicon-gcp 0.0.18 → 0.0.22

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 (47) hide show
  1. package/dist/integrity.json +8 -4
  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/wgc401.ts +59 -0
  6. package/dist/rules/wgc402.ts +54 -0
  7. package/dist/rules/wgc403.ts +84 -0
  8. package/dist/skills/chant-gcp.md +4 -1
  9. package/package.json +20 -2
  10. package/src/codegen/docs.test.ts +16 -0
  11. package/src/codegen/generate.test.ts +18 -0
  12. package/src/codegen/generate.ts +11 -0
  13. package/src/codegen/package.test.ts +16 -0
  14. package/src/generated/lexicon-gcp.json +18141 -0
  15. package/src/import/import-fixtures.test.ts +98 -0
  16. package/src/lint/post-synth/gcp-helpers.test.ts +166 -0
  17. package/src/lint/post-synth/post-synth.test.ts +130 -0
  18. package/src/lint/post-synth/schema-registry.ts +91 -0
  19. package/src/lint/post-synth/wgc101.test.ts +39 -0
  20. package/src/lint/post-synth/wgc102.test.ts +38 -0
  21. package/src/lint/post-synth/wgc103.test.ts +38 -0
  22. package/src/lint/post-synth/wgc104.test.ts +37 -0
  23. package/src/lint/post-synth/wgc105.test.ts +46 -0
  24. package/src/lint/post-synth/wgc106.test.ts +38 -0
  25. package/src/lint/post-synth/wgc107.test.ts +38 -0
  26. package/src/lint/post-synth/wgc108.test.ts +42 -0
  27. package/src/lint/post-synth/wgc109.test.ts +46 -0
  28. package/src/lint/post-synth/wgc110.test.ts +37 -0
  29. package/src/lint/post-synth/wgc111.test.ts +46 -0
  30. package/src/lint/post-synth/wgc112.test.ts +48 -0
  31. package/src/lint/post-synth/wgc113.test.ts +36 -0
  32. package/src/lint/post-synth/wgc201.test.ts +38 -0
  33. package/src/lint/post-synth/wgc202.test.ts +38 -0
  34. package/src/lint/post-synth/wgc203.test.ts +45 -0
  35. package/src/lint/post-synth/wgc204.test.ts +42 -0
  36. package/src/lint/post-synth/wgc301.test.ts +39 -0
  37. package/src/lint/post-synth/wgc302.test.ts +36 -0
  38. package/src/lint/post-synth/wgc303.test.ts +37 -0
  39. package/src/lint/post-synth/wgc401.test.ts +46 -0
  40. package/src/lint/post-synth/wgc401.ts +59 -0
  41. package/src/lint/post-synth/wgc402.test.ts +40 -0
  42. package/src/lint/post-synth/wgc402.ts +54 -0
  43. package/src/lint/post-synth/wgc403.test.ts +59 -0
  44. package/src/lint/post-synth/wgc403.ts +84 -0
  45. package/src/plugin.test.ts +4 -1
  46. package/src/plugin.ts +172 -3
  47. package/src/skills/chant-gcp.md +4 -1
@@ -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
+ });
@@ -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 {
@@ -709,3 +712,130 @@ spec:
709
712
  expect(diags).toHaveLength(0);
710
713
  });
711
714
  });
715
+
716
+ // ── WGC401: Unknown spec field ─────────────────────────────────────
717
+
718
+ describe("WGC401: unknown spec field", () => {
719
+ test("flags unknown field with did-you-mean suggestion", () => {
720
+ const yaml = `apiVersion: compute.cnrm.cloud.google.com/v1beta1
721
+ kind: ComputeFirewall
722
+ metadata:
723
+ name: my-fw
724
+ spec:
725
+ allowed:
726
+ - protocol: tcp
727
+ networkRef:
728
+ name: my-network
729
+ `;
730
+ const diags = wgc401.check(makeCtx(yaml));
731
+ // "allowed" is not a valid field — "allow" is. Should flag it.
732
+ const unknownDiags = diags.filter(d => d.checkId === "WGC401");
733
+ // If schema is loaded, this will flag "allowed"
734
+ // If schema is not loaded (pre-generate), skip gracefully
735
+ if (unknownDiags.length > 0) {
736
+ expect(unknownDiags[0].severity).toBe("error");
737
+ expect(unknownDiags[0].message).toContain("allowed");
738
+ }
739
+ });
740
+
741
+ test("no diagnostic for valid fields", () => {
742
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
743
+ kind: StorageBucket
744
+ metadata:
745
+ name: my-bucket
746
+ spec:
747
+ location: US
748
+ `;
749
+ const diags = wgc401.check(makeCtx(yaml));
750
+ // "location" is a valid StorageBucket field
751
+ const unknownDiags = diags.filter(d => d.checkId === "WGC401");
752
+ expect(unknownDiags).toHaveLength(0);
753
+ });
754
+ });
755
+
756
+ // ── WGC402: Missing required field ─────────────────────────────────
757
+
758
+ describe("WGC402: missing required field", () => {
759
+ test("flags missing required field", () => {
760
+ const yaml = `apiVersion: compute.cnrm.cloud.google.com/v1beta1
761
+ kind: ComputeAddress
762
+ metadata:
763
+ name: my-addr
764
+ spec:
765
+ description: test
766
+ `;
767
+ const diags = wgc402.check(makeCtx(yaml));
768
+ const requiredDiags = diags.filter(d => d.checkId === "WGC402");
769
+ // ComputeAddress requires "location" — if schema is loaded, this flags it
770
+ if (requiredDiags.length > 0) {
771
+ expect(requiredDiags[0].severity).toBe("error");
772
+ expect(requiredDiags[0].message).toContain("required");
773
+ }
774
+ });
775
+
776
+ test("no diagnostic when all required fields present", () => {
777
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
778
+ kind: StorageBucket
779
+ metadata:
780
+ name: my-bucket
781
+ spec:
782
+ location: US
783
+ `;
784
+ const diags = wgc402.check(makeCtx(yaml));
785
+ const requiredDiags = diags.filter(d => d.checkId === "WGC402");
786
+ expect(requiredDiags).toHaveLength(0);
787
+ });
788
+ });
789
+
790
+ // ── WGC403: Type/structure mismatch ────────────────────────────────
791
+
792
+ describe("WGC403: type/structure mismatch", () => {
793
+ test("flags string where number expected", () => {
794
+ const yaml = `apiVersion: cloudfunctions.cnrm.cloud.google.com/v1beta1
795
+ kind: CloudFunctionsFunction
796
+ metadata:
797
+ name: my-fn
798
+ spec:
799
+ runtime: nodejs18
800
+ availableMemoryMb: "512"
801
+ region: us-central1
802
+ `;
803
+ const diags = wgc403.check(makeCtx(yaml));
804
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
805
+ if (typeDiags.length > 0) {
806
+ expect(typeDiags[0].severity).toBe("error");
807
+ expect(typeDiags[0].message).toContain("number");
808
+ expect(typeDiags[0].message).toContain("string");
809
+ }
810
+ });
811
+
812
+ test("flags bare string instead of resourceRef object", () => {
813
+ const yaml = `apiVersion: pubsub.cnrm.cloud.google.com/v1beta1
814
+ kind: PubSubSubscription
815
+ metadata:
816
+ name: my-sub
817
+ spec:
818
+ topicRef: my-topic
819
+ `;
820
+ const diags = wgc403.check(makeCtx(yaml));
821
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
822
+ if (typeDiags.length > 0) {
823
+ expect(typeDiags[0].severity).toBe("error");
824
+ expect(typeDiags[0].message).toContain("resourceRef");
825
+ }
826
+ });
827
+
828
+ test("no diagnostic with correct types", () => {
829
+ const yaml = `apiVersion: storage.cnrm.cloud.google.com/v1beta1
830
+ kind: StorageBucket
831
+ metadata:
832
+ name: my-bucket
833
+ spec:
834
+ location: US
835
+ uniformBucketLevelAccess: true
836
+ `;
837
+ const diags = wgc403.check(makeCtx(yaml));
838
+ const typeDiags = diags.filter(d => d.checkId === "WGC403");
839
+ expect(typeDiags).toHaveLength(0);
840
+ });
841
+ });
@@ -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,39 @@
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
+ defaultKmsKeyName: projects/p/locations/l/keyRings/kr/cryptoKeys/k
34
+ `;
35
+ const diags = wgc101.check(makeCtx(yaml));
36
+ const bucketDiags = diags.filter(d => d.entity === "my-bucket");
37
+ expect(bucketDiags).toHaveLength(0);
38
+ });
39
+ });
@@ -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
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { wgc105 } from "./wgc105";
3
+
4
+ function makeCtx(yaml: string) {
5
+ return {
6
+ outputs: new Map([["gcp", yaml]]),
7
+ };
8
+ }
9
+
10
+ describe("WGC105: public Cloud SQL", () => {
11
+ test("flags SQLInstance with 0.0.0.0/0 in authorizedNetworks", () => {
12
+ const yaml = `apiVersion: sql.cnrm.cloud.google.com/v1beta1
13
+ kind: SQLInstance
14
+ metadata:
15
+ name: my-db
16
+ spec:
17
+ databaseVersion: POSTGRES_15
18
+ settings:
19
+ ipConfiguration:
20
+ authorizedNetworks:
21
+ - value: "0.0.0.0/0"
22
+ name: public
23
+ `;
24
+ const diags = wgc105.check(makeCtx(yaml));
25
+ expect(diags.length).toBeGreaterThanOrEqual(1);
26
+ expect(diags[0].checkId).toBe("WGC105");
27
+ expect(diags[0].severity).toBe("warning");
28
+ });
29
+
30
+ test("no diagnostic with private networks only", () => {
31
+ const yaml = `apiVersion: sql.cnrm.cloud.google.com/v1beta1
32
+ kind: SQLInstance
33
+ metadata:
34
+ name: my-db
35
+ spec:
36
+ databaseVersion: POSTGRES_15
37
+ settings:
38
+ ipConfiguration:
39
+ authorizedNetworks:
40
+ - value: "10.0.0.0/8"
41
+ name: internal
42
+ `;
43
+ const diags = wgc105.check(makeCtx(yaml));
44
+ expect(diags).toHaveLength(0);
45
+ });
46
+ });