@intentius/chant-lexicon-gcp 0.124.31 → 0.124.32
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.
- package/dist/actions/index.js +45 -0
- package/dist/api/read-client.js +105 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +115 -0
- package/dist/codegen/generate-cli.js +17 -0
- package/dist/codegen/generate.js +214 -0
- package/dist/codegen/naming.js +164 -0
- package/dist/codegen/package.js +43 -0
- package/dist/composites/catalog.js +1245 -0
- package/dist/composites/cloud-function.js +65 -0
- package/dist/composites/cloud-run-service.js +71 -0
- package/dist/composites/cloud-sql-instance.js +85 -0
- package/dist/composites/gcs-bucket.js +64 -0
- package/dist/composites/gke-cluster.js +113 -0
- package/dist/composites/gke-crdb-region.js +137 -0
- package/dist/composites/gpu-node-pool.js +74 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/landing-zone.js +89 -0
- package/dist/composites/managed-certificate.js +46 -0
- package/dist/composites/memorystore-redis.js +52 -0
- package/dist/composites/multi-region-vpc.js +108 -0
- package/dist/composites/private-service.js +50 -0
- package/dist/composites/pubsub-pipeline.js +63 -0
- package/dist/composites/secure-project.js +85 -0
- package/dist/composites/vpc-network.js +113 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe-hooks.js +152 -0
- package/dist/deep-observe.js +273 -0
- package/dist/default-labels.js +67 -0
- package/dist/describe-resources.js +264 -0
- package/dist/detect.js +16 -0
- package/dist/export-resources.js +59 -0
- package/dist/generated/index.js +1804 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +98 -0
- package/dist/import/generator.js +68 -0
- package/dist/import/live-export.js +73 -0
- package/dist/import/parser.js +61 -0
- package/dist/index.js +36 -0
- package/dist/integrity.json +3 -3
- package/dist/lint/audit-catalog.js +38 -0
- package/dist/lint/audit-lineage.js +63 -0
- package/dist/lint/post-synth/gcp-helpers.js +91 -0
- package/dist/lint/post-synth/index.js +54 -0
- package/dist/lint/post-synth/schema-registry.d.ts.map +1 -1
- package/dist/lint/post-synth/schema-registry.js +69 -0
- package/dist/lint/post-synth/wgc101.js +47 -0
- package/dist/lint/post-synth/wgc102.js +29 -0
- package/dist/lint/post-synth/wgc103.js +37 -0
- package/dist/lint/post-synth/wgc104.js +34 -0
- package/dist/lint/post-synth/wgc105.js +39 -0
- package/dist/lint/post-synth/wgc106.js +30 -0
- package/dist/lint/post-synth/wgc107.js +33 -0
- package/dist/lint/post-synth/wgc108.js +34 -0
- package/dist/lint/post-synth/wgc109.js +33 -0
- package/dist/lint/post-synth/wgc110.js +32 -0
- package/dist/lint/post-synth/wgc111.js +47 -0
- package/dist/lint/post-synth/wgc112.js +46 -0
- package/dist/lint/post-synth/wgc113.js +35 -0
- package/dist/lint/post-synth/wgc201.js +30 -0
- package/dist/lint/post-synth/wgc202.js +33 -0
- package/dist/lint/post-synth/wgc203.js +35 -0
- package/dist/lint/post-synth/wgc204.js +33 -0
- package/dist/lint/post-synth/wgc301.js +28 -0
- package/dist/lint/post-synth/wgc302.js +28 -0
- package/dist/lint/post-synth/wgc303.js +29 -0
- package/dist/lint/post-synth/wgc401.js +50 -0
- package/dist/lint/post-synth/wgc402.js +46 -0
- package/dist/lint/post-synth/wgc403.js +74 -0
- package/dist/lint/post-synth/wgc501.js +63 -0
- package/dist/lint/post-synth/wgc502.js +37 -0
- package/dist/lint/post-synth/wgc503.js +49 -0
- package/dist/lint/rules/hardcoded-project.js +51 -0
- package/dist/lint/rules/hardcoded-region.js +51 -0
- package/dist/lint/rules/index.js +3 -0
- package/dist/lint/rules/public-iam.js +37 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +15 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +46 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/floci-gcp.js +49 -0
- package/dist/op/activities/gcp-apply.js +618 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +50 -0
- package/dist/ownership.js +38 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +371 -0
- package/dist/pseudo.js +15 -0
- package/dist/rules/schema-registry.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +233 -0
- package/dist/spec/fetch.js +107 -0
- package/dist/spec/parse.js +323 -0
- package/dist/validate-cli.js +11 -0
- package/dist/validate.js +24 -0
- package/dist/variables.js +15 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
- package/src/lint/post-synth/schema-registry.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC204: ComputeInstance without shielded VM config
|
|
3
|
+
*/
|
|
4
|
+
import { parseGcpManifests, getSpec, getResourceName } from "./gcp-helpers.js";
|
|
5
|
+
export const wgc204 = {
|
|
6
|
+
id: "WGC204",
|
|
7
|
+
description: "ComputeInstance without shielded VM configuration",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const [, output] of ctx.outputs) {
|
|
11
|
+
if (typeof output !== "string")
|
|
12
|
+
continue;
|
|
13
|
+
for (const manifest of parseGcpManifests(output)) {
|
|
14
|
+
if (manifest.kind !== "ComputeInstance")
|
|
15
|
+
continue;
|
|
16
|
+
const spec = getSpec(manifest);
|
|
17
|
+
if (!spec)
|
|
18
|
+
continue;
|
|
19
|
+
const shieldedConfig = spec.shieldedInstanceConfig;
|
|
20
|
+
if (!shieldedConfig) {
|
|
21
|
+
diagnostics.push({
|
|
22
|
+
checkId: "WGC204",
|
|
23
|
+
severity: "info",
|
|
24
|
+
message: `ComputeInstance "${getResourceName(manifest)}" has no shielded VM configuration — consider enabling secureboot, vtpm, and integrity monitoring`,
|
|
25
|
+
entity: getResourceName(manifest),
|
|
26
|
+
lexicon: "gcp",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return diagnostics;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC301: No audit logging config (IAMAuditConfig) in output
|
|
3
|
+
*/
|
|
4
|
+
import { parseGcpManifests } from "./gcp-helpers.js";
|
|
5
|
+
export const wgc301 = {
|
|
6
|
+
id: "WGC301",
|
|
7
|
+
description: "No IAMAuditConfig resource found in output — audit logging may not be configured",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const [, output] of ctx.outputs) {
|
|
11
|
+
if (typeof output !== "string")
|
|
12
|
+
continue;
|
|
13
|
+
const manifests = parseGcpManifests(output);
|
|
14
|
+
if (manifests.length === 0)
|
|
15
|
+
continue;
|
|
16
|
+
const hasAuditConfig = manifests.some(m => m.kind === "IAMAuditConfig");
|
|
17
|
+
if (!hasAuditConfig) {
|
|
18
|
+
diagnostics.push({
|
|
19
|
+
checkId: "WGC301",
|
|
20
|
+
severity: "info",
|
|
21
|
+
message: "No IAMAuditConfig resource found — consider adding audit logging for compliance",
|
|
22
|
+
lexicon: "gcp",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return diagnostics;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC302: Service API not explicitly enabled
|
|
3
|
+
*/
|
|
4
|
+
import { parseGcpManifests } from "./gcp-helpers.js";
|
|
5
|
+
export const wgc302 = {
|
|
6
|
+
id: "WGC302",
|
|
7
|
+
description: "No Service resource found — GCP APIs may not be explicitly enabled",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const [, output] of ctx.outputs) {
|
|
11
|
+
if (typeof output !== "string")
|
|
12
|
+
continue;
|
|
13
|
+
const manifests = parseGcpManifests(output);
|
|
14
|
+
if (manifests.length === 0)
|
|
15
|
+
continue;
|
|
16
|
+
const hasService = manifests.some(m => m.kind === "Service" && m.apiVersion?.includes("serviceusage.cnrm"));
|
|
17
|
+
if (!hasService) {
|
|
18
|
+
diagnostics.push({
|
|
19
|
+
checkId: "WGC302",
|
|
20
|
+
severity: "info",
|
|
21
|
+
message: "No Service (serviceusage) resource found — consider explicitly enabling required GCP APIs",
|
|
22
|
+
lexicon: "gcp",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return diagnostics;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC303: Missing VPC Service Controls perimeter
|
|
3
|
+
*/
|
|
4
|
+
import { parseGcpManifests } from "./gcp-helpers.js";
|
|
5
|
+
export const wgc303 = {
|
|
6
|
+
id: "WGC303",
|
|
7
|
+
description: "No AccessContextManager ServicePerimeter found — VPC Service Controls not configured",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const [, output] of ctx.outputs) {
|
|
11
|
+
if (typeof output !== "string")
|
|
12
|
+
continue;
|
|
13
|
+
const manifests = parseGcpManifests(output);
|
|
14
|
+
if (manifests.length === 0)
|
|
15
|
+
continue;
|
|
16
|
+
const hasPerimeter = manifests.some(m => m.kind === "AccessContextManagerServicePerimeter" ||
|
|
17
|
+
(m.apiVersion?.includes("accesscontextmanager") && m.kind?.includes("ServicePerimeter")));
|
|
18
|
+
if (!hasPerimeter) {
|
|
19
|
+
diagnostics.push({
|
|
20
|
+
checkId: "WGC303",
|
|
21
|
+
severity: "info",
|
|
22
|
+
message: "No VPC Service Controls perimeter found — consider adding for data exfiltration protection",
|
|
23
|
+
lexicon: "gcp",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return diagnostics;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC401: Unknown spec field
|
|
3
|
+
*
|
|
4
|
+
* Flags fields in a Config Connector resource spec that are not defined
|
|
5
|
+
* in the CRD schema. Includes "did you mean?" suggestion via Levenshtein.
|
|
6
|
+
*/
|
|
7
|
+
import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers.js";
|
|
8
|
+
import { getSchemaRegistry, suggestField } from "./schema-registry.js";
|
|
9
|
+
export const wgc401 = {
|
|
10
|
+
id: "WGC401",
|
|
11
|
+
description: "Config Connector resource spec contains unknown field",
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
const registry = getSchemaRegistry();
|
|
15
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
16
|
+
if (typeof output !== "string")
|
|
17
|
+
continue;
|
|
18
|
+
const manifests = parseGcpManifests(output);
|
|
19
|
+
for (const manifest of manifests) {
|
|
20
|
+
if (!isConfigConnectorResource(manifest))
|
|
21
|
+
continue;
|
|
22
|
+
const kind = manifest.kind;
|
|
23
|
+
if (!kind)
|
|
24
|
+
continue;
|
|
25
|
+
const schema = registry.get(kind);
|
|
26
|
+
if (!schema)
|
|
27
|
+
continue; // No schema available for this kind
|
|
28
|
+
const spec = getSpec(manifest);
|
|
29
|
+
if (!spec)
|
|
30
|
+
continue;
|
|
31
|
+
const knownFields = Object.keys(schema.fields);
|
|
32
|
+
const resourceName = getResourceName(manifest);
|
|
33
|
+
for (const field of Object.keys(spec)) {
|
|
34
|
+
if (field in schema.fields)
|
|
35
|
+
continue;
|
|
36
|
+
const suggestion = suggestField(field, knownFields);
|
|
37
|
+
const hint = suggestion ? ` — did you mean "${suggestion}"?` : "";
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "WGC401",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `Resource "${resourceName}" (${kind}): unknown spec field "${field}"${hint}`,
|
|
42
|
+
entity: resourceName,
|
|
43
|
+
lexicon: "gcp",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return diagnostics;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC402: Missing required spec field
|
|
3
|
+
*
|
|
4
|
+
* Flags Config Connector resources that are missing required fields
|
|
5
|
+
* according to their CRD schema.
|
|
6
|
+
*/
|
|
7
|
+
import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers.js";
|
|
8
|
+
import { getSchemaRegistry } from "./schema-registry.js";
|
|
9
|
+
export const wgc402 = {
|
|
10
|
+
id: "WGC402",
|
|
11
|
+
description: "Config Connector resource is missing a required spec field",
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
const registry = getSchemaRegistry();
|
|
15
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
16
|
+
if (typeof output !== "string")
|
|
17
|
+
continue;
|
|
18
|
+
const manifests = parseGcpManifests(output);
|
|
19
|
+
for (const manifest of manifests) {
|
|
20
|
+
if (!isConfigConnectorResource(manifest))
|
|
21
|
+
continue;
|
|
22
|
+
const kind = manifest.kind;
|
|
23
|
+
if (!kind)
|
|
24
|
+
continue;
|
|
25
|
+
const schema = registry.get(kind);
|
|
26
|
+
if (!schema)
|
|
27
|
+
continue;
|
|
28
|
+
const spec = getSpec(manifest);
|
|
29
|
+
const specKeys = new Set(spec ? Object.keys(spec) : []);
|
|
30
|
+
const resourceName = getResourceName(manifest);
|
|
31
|
+
for (const requiredField of schema.required) {
|
|
32
|
+
if (!specKeys.has(requiredField)) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
checkId: "WGC402",
|
|
35
|
+
severity: "error",
|
|
36
|
+
message: `Resource "${resourceName}" (${kind}): missing required field "${requiredField}"`,
|
|
37
|
+
entity: resourceName,
|
|
38
|
+
lexicon: "gcp",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return diagnostics;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC403: Type/structure mismatch in spec field
|
|
3
|
+
*
|
|
4
|
+
* Flags spec fields where the value type doesn't match the CRD schema:
|
|
5
|
+
* - String where number expected (e.g. availableMemoryMb: "512")
|
|
6
|
+
* - Scalar string where resourceRef object expected (e.g. topicRef: "name")
|
|
7
|
+
*/
|
|
8
|
+
import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers.js";
|
|
9
|
+
import { getSchemaRegistry } from "./schema-registry.js";
|
|
10
|
+
export const wgc403 = {
|
|
11
|
+
id: "WGC403",
|
|
12
|
+
description: "Config Connector resource spec field has wrong type or structure",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
const registry = getSchemaRegistry();
|
|
16
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
17
|
+
if (typeof output !== "string")
|
|
18
|
+
continue;
|
|
19
|
+
const manifests = parseGcpManifests(output);
|
|
20
|
+
for (const manifest of manifests) {
|
|
21
|
+
if (!isConfigConnectorResource(manifest))
|
|
22
|
+
continue;
|
|
23
|
+
const kind = manifest.kind;
|
|
24
|
+
if (!kind)
|
|
25
|
+
continue;
|
|
26
|
+
const schema = registry.get(kind);
|
|
27
|
+
if (!schema)
|
|
28
|
+
continue;
|
|
29
|
+
const spec = getSpec(manifest);
|
|
30
|
+
if (!spec)
|
|
31
|
+
continue;
|
|
32
|
+
const resourceName = getResourceName(manifest);
|
|
33
|
+
for (const [field, value] of Object.entries(spec)) {
|
|
34
|
+
const fieldSchema = schema.fields[field];
|
|
35
|
+
if (!fieldSchema)
|
|
36
|
+
continue; // Unknown fields handled by WGC401
|
|
37
|
+
// Check: resourceRef field expects an object, got a scalar
|
|
38
|
+
if (fieldSchema.ref && typeof value === "string") {
|
|
39
|
+
diagnostics.push({
|
|
40
|
+
checkId: "WGC403",
|
|
41
|
+
severity: "error",
|
|
42
|
+
message: `Resource "${resourceName}" (${kind}): field "${field}" expects a resourceRef object (e.g. { name, kind }), got string "${value}"`,
|
|
43
|
+
entity: resourceName,
|
|
44
|
+
lexicon: "gcp",
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Check: number field got a string that looks numeric
|
|
49
|
+
if (fieldSchema.type === "number" && typeof value === "string") {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "WGC403",
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: `Resource "${resourceName}" (${kind}): field "${field}" expects a number, got string "${value}"`,
|
|
54
|
+
entity: resourceName,
|
|
55
|
+
lexicon: "gcp",
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Check: boolean field got a string
|
|
60
|
+
if (fieldSchema.type === "boolean" && typeof value === "string") {
|
|
61
|
+
diagnostics.push({
|
|
62
|
+
checkId: "WGC403",
|
|
63
|
+
severity: "error",
|
|
64
|
+
message: `Resource "${resourceName}" (${kind}): field "${field}" expects a boolean, got string "${value}"`,
|
|
65
|
+
entity: resourceName,
|
|
66
|
+
lexicon: "gcp",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return diagnostics;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC501: Org Policy Guardrail Not Enforced
|
|
3
|
+
*
|
|
4
|
+
* Flags OrgPolicyPolicy resources whose rules switch enforcement off or that
|
|
5
|
+
* reset the constraint to its default. A disabled org policy is the GCP
|
|
6
|
+
* shape of a weakened guardrail: the constraint stays visible in the tree
|
|
7
|
+
* while constraining nothing.
|
|
8
|
+
*/
|
|
9
|
+
import { parseGcpManifests, getResourceName } from "./gcp-helpers.js";
|
|
10
|
+
/**
|
|
11
|
+
* OrgPolicyPolicy mirrors the API's own `spec` inside the CRD spec, so rules
|
|
12
|
+
* live at `spec.spec.rules`; accept `spec.rules` too for hand-written manifests.
|
|
13
|
+
*/
|
|
14
|
+
export function orgPolicySpec(manifest) {
|
|
15
|
+
const outer = manifest.spec ?? {};
|
|
16
|
+
const inner = outer.spec;
|
|
17
|
+
return typeof inner === "object" && inner !== null ? inner : outer;
|
|
18
|
+
}
|
|
19
|
+
export function orgPolicyRules(manifest) {
|
|
20
|
+
const rules = orgPolicySpec(manifest).rules;
|
|
21
|
+
return Array.isArray(rules) ? rules : [];
|
|
22
|
+
}
|
|
23
|
+
function isDisabled(rule) {
|
|
24
|
+
return rule.enforce === false || rule.enforce === "FALSE";
|
|
25
|
+
}
|
|
26
|
+
export const wgc501 = {
|
|
27
|
+
id: "WGC501",
|
|
28
|
+
description: "Org Policy guardrail not enforced — a disabled policy constrains nothing",
|
|
29
|
+
check(ctx) {
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
for (const [, output] of ctx.outputs) {
|
|
32
|
+
if (typeof output !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
for (const manifest of parseGcpManifests(output)) {
|
|
35
|
+
if (manifest.kind !== "OrgPolicyPolicy")
|
|
36
|
+
continue;
|
|
37
|
+
const name = getResourceName(manifest);
|
|
38
|
+
if (orgPolicySpec(manifest).reset === true) {
|
|
39
|
+
diagnostics.push({
|
|
40
|
+
checkId: "WGC501",
|
|
41
|
+
severity: "error",
|
|
42
|
+
message: `OrgPolicyPolicy "${name}" resets the constraint to its default — the guardrail is switched off`,
|
|
43
|
+
entity: name,
|
|
44
|
+
lexicon: "gcp",
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const rule of orgPolicyRules(manifest)) {
|
|
49
|
+
if (isDisabled(rule)) {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "WGC501",
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: `OrgPolicyPolicy "${name}" has a rule with enforce disabled — the guardrail is declared but not enforced`,
|
|
54
|
+
entity: name,
|
|
55
|
+
lexicon: "gcp",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return diagnostics;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC502: Org Policy Guardrail Defines No Rules
|
|
3
|
+
*
|
|
4
|
+
* Flags OrgPolicyPolicy resources with no rules (and no reset). A rule-less
|
|
5
|
+
* org policy binds a constraint to nothing — the usual leftover of a
|
|
6
|
+
* guardrail whose rules were removed while the policy object was kept.
|
|
7
|
+
*/
|
|
8
|
+
import { parseGcpManifests, getResourceName } from "./gcp-helpers.js";
|
|
9
|
+
import { orgPolicyRules, orgPolicySpec } from "./wgc501.js";
|
|
10
|
+
export const wgc502 = {
|
|
11
|
+
id: "WGC502",
|
|
12
|
+
description: "Org Policy guardrail defines no rules — it binds its constraint to nothing",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const [, output] of ctx.outputs) {
|
|
16
|
+
if (typeof output !== "string")
|
|
17
|
+
continue;
|
|
18
|
+
for (const manifest of parseGcpManifests(output)) {
|
|
19
|
+
if (manifest.kind !== "OrgPolicyPolicy")
|
|
20
|
+
continue;
|
|
21
|
+
if (orgPolicySpec(manifest).reset === true)
|
|
22
|
+
continue; // WGC501's finding
|
|
23
|
+
if (orgPolicyRules(manifest).length > 0)
|
|
24
|
+
continue;
|
|
25
|
+
const name = getResourceName(manifest);
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
checkId: "WGC502",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `OrgPolicyPolicy "${name}" defines no rules — the guardrail constrains nothing`,
|
|
30
|
+
entity: name,
|
|
31
|
+
lexicon: "gcp",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGC503: Audit Logging Dropped or Scoped Down
|
|
3
|
+
*
|
|
4
|
+
* Flags IAMAuditConfig resources that capture nothing (no auditLogConfigs)
|
|
5
|
+
* or that carve identities out of the audit trail via exemptedMembers.
|
|
6
|
+
* WGC301 reports the absence of any IAMAuditConfig as hygiene; this check
|
|
7
|
+
* covers the declared-but-weakened sink, which is merge-worthy.
|
|
8
|
+
*/
|
|
9
|
+
import { parseGcpManifests, getResourceName, getSpec } from "./gcp-helpers.js";
|
|
10
|
+
export const wgc503 = {
|
|
11
|
+
id: "WGC503",
|
|
12
|
+
description: "IAMAuditConfig captures nothing or exempts members — the audit sink is scoped down",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const [, output] of ctx.outputs) {
|
|
16
|
+
if (typeof output !== "string")
|
|
17
|
+
continue;
|
|
18
|
+
for (const manifest of parseGcpManifests(output)) {
|
|
19
|
+
if (manifest.kind !== "IAMAuditConfig")
|
|
20
|
+
continue;
|
|
21
|
+
const name = getResourceName(manifest);
|
|
22
|
+
const configs = getSpec(manifest)?.auditLogConfigs;
|
|
23
|
+
if (!Array.isArray(configs) || configs.length === 0) {
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
checkId: "WGC503",
|
|
26
|
+
severity: "error",
|
|
27
|
+
message: `IAMAuditConfig "${name}" has no auditLogConfigs — the audit sink is declared but captures nothing`,
|
|
28
|
+
entity: name,
|
|
29
|
+
lexicon: "gcp",
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const config of configs) {
|
|
34
|
+
const exempted = config.exemptedMembers;
|
|
35
|
+
if (Array.isArray(exempted) && exempted.length > 0) {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
checkId: "WGC503",
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: `IAMAuditConfig "${name}" exempts members from ${String(config.logType ?? "audit")} logging — audit evidence is scoped down`,
|
|
40
|
+
entity: name,
|
|
41
|
+
lexicon: "gcp",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return diagnostics;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WGC001: Hardcoded GCP Project ID
|
|
4
|
+
*
|
|
5
|
+
* Detects hardcoded GCP project IDs in code.
|
|
6
|
+
* Project IDs should use GCP.ProjectId pseudo-parameter instead.
|
|
7
|
+
*/
|
|
8
|
+
export const hardcodedProjectRule = {
|
|
9
|
+
id: "WGC001",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "security",
|
|
12
|
+
description: "Detects hardcoded GCP project IDs — use GCP.ProjectId pseudo-parameter instead",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
// GCP project ID pattern: lowercase letters, digits, hyphens, 6-30 chars
|
|
17
|
+
// We look for strings in annotation contexts like "cnrm.cloud.google.com/project-id"
|
|
18
|
+
const projectIdPattern = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
19
|
+
// Common false positives
|
|
20
|
+
const falsePositives = new Set([
|
|
21
|
+
"chant", "default", "kube-system", "kube-public", "kube-node-lease",
|
|
22
|
+
"config-connector", "cnrm-system", "my-project",
|
|
23
|
+
]);
|
|
24
|
+
function visit(node) {
|
|
25
|
+
if (ts.isPropertyAssignment(node) || ts.isPropertyDeclaration(node)) {
|
|
26
|
+
const propName = node.name?.getText(sourceFile) ?? "";
|
|
27
|
+
// Only flag project-id annotation values
|
|
28
|
+
if (propName.includes("project-id") || propName.includes("projectId") || propName === '"cnrm.cloud.google.com/project-id"') {
|
|
29
|
+
const initializer = ts.isPropertyAssignment(node) ? node.initializer : undefined;
|
|
30
|
+
if (initializer && ts.isStringLiteral(initializer)) {
|
|
31
|
+
const value = initializer.text;
|
|
32
|
+
if (projectIdPattern.test(value) && !falsePositives.has(value)) {
|
|
33
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(initializer.getStart());
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
file: sourceFile.fileName,
|
|
36
|
+
line: line + 1,
|
|
37
|
+
column: character + 1,
|
|
38
|
+
ruleId: "WGC001",
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message: `Hardcoded project ID "${value}" detected. Use GCP.ProjectId pseudo-parameter instead.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
ts.forEachChild(node, visit);
|
|
47
|
+
}
|
|
48
|
+
visit(sourceFile);
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WGC002: Hardcoded GCP Region/Zone
|
|
4
|
+
*
|
|
5
|
+
* Detects hardcoded GCP region and zone strings in code.
|
|
6
|
+
* Regions/zones should use GCP.Region or GCP.Zone pseudo-parameters instead.
|
|
7
|
+
*/
|
|
8
|
+
export const hardcodedRegionRule = {
|
|
9
|
+
id: "WGC002",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "security",
|
|
12
|
+
description: "Detects hardcoded GCP region/zone strings — use GCP.Region/GCP.Zone pseudo-parameters instead",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
// GCP region pattern: continent-direction[N] (e.g., us-central1, europe-west4)
|
|
17
|
+
const regionPattern = /^(us|europe|asia|australia|southamerica|northamerica|me|africa)-(central|east|west|south|north|northeast|southeast|southwest|northwest)\d+$/;
|
|
18
|
+
// GCP zone pattern: region-[a-f] (e.g., us-central1-a)
|
|
19
|
+
const zonePattern = /^(us|europe|asia|australia|southamerica|northamerica|me|africa)-(central|east|west|south|north|northeast|southeast|southwest|northwest)\d+-[a-f]$/;
|
|
20
|
+
function visit(node) {
|
|
21
|
+
if (ts.isStringLiteral(node)) {
|
|
22
|
+
const value = node.text;
|
|
23
|
+
if (zonePattern.test(value)) {
|
|
24
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
file: sourceFile.fileName,
|
|
27
|
+
line: line + 1,
|
|
28
|
+
column: character + 1,
|
|
29
|
+
ruleId: "WGC002",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
message: `Hardcoded zone "${value}" detected. Use GCP.Zone pseudo-parameter instead.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else if (regionPattern.test(value)) {
|
|
35
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
file: sourceFile.fileName,
|
|
38
|
+
line: line + 1,
|
|
39
|
+
column: character + 1,
|
|
40
|
+
ruleId: "WGC002",
|
|
41
|
+
severity: "warning",
|
|
42
|
+
message: `Hardcoded region "${value}" detected. Use GCP.Region pseudo-parameter instead.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
ts.forEachChild(node, visit);
|
|
47
|
+
}
|
|
48
|
+
visit(sourceFile);
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WGC003: Public IAM Binding
|
|
4
|
+
*
|
|
5
|
+
* Warns on allUsers or allAuthenticatedUsers IAM bindings,
|
|
6
|
+
* which grant public access to GCP resources.
|
|
7
|
+
*/
|
|
8
|
+
export const publicIamRule = {
|
|
9
|
+
id: "WGC003",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "security",
|
|
12
|
+
description: "Warns on allUsers/allAuthenticatedUsers IAM members — grants public access",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
const publicMembers = new Set(["allUsers", "allAuthenticatedUsers"]);
|
|
17
|
+
function visit(node) {
|
|
18
|
+
if (ts.isStringLiteral(node)) {
|
|
19
|
+
const value = node.text;
|
|
20
|
+
if (publicMembers.has(value)) {
|
|
21
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
file: sourceFile.fileName,
|
|
24
|
+
line: line + 1,
|
|
25
|
+
column: character + 1,
|
|
26
|
+
ruleId: "WGC003",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `Public IAM member "${value}" detected. This grants access to everyone${value === "allAuthenticatedUsers" ? " with a Google account" : " on the internet"}.`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(node, visit);
|
|
33
|
+
}
|
|
34
|
+
visit(sourceFile);
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEvE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-gcp.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
export function gcpCompletions(ctx) {
|
|
14
|
+
return lexiconCompletions(ctx, getIndex(), "GCP Config Connector resource");
|
|
15
|
+
}
|
package/dist/lsp/hover.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAqB1E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAEjE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-gcp.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
export function gcpHover(ctx) {
|
|
14
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
15
|
+
}
|
|
16
|
+
function resourceHover(className, entry) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
const gcpEntry = entry;
|
|
19
|
+
lines.push(`**${className}**`);
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push(`GCP Config Connector resource: \`${entry.resourceType}\``);
|
|
22
|
+
if (gcpEntry.apiVersion) {
|
|
23
|
+
lines.push(`API Version: \`${gcpEntry.apiVersion}\``);
|
|
24
|
+
}
|
|
25
|
+
if (gcpEntry.gvkKind) {
|
|
26
|
+
lines.push(`Kind: \`${gcpEntry.gvkKind}\``);
|
|
27
|
+
}
|
|
28
|
+
const customAttrs = Object.entries(entry.attrs ?? {})
|
|
29
|
+
.filter(([k]) => k !== "apiVersion" && k !== "kind" && k !== "name" && k !== "namespace" && k !== "uid");
|
|
30
|
+
if (customAttrs.length > 0) {
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push("**Attributes:**");
|
|
33
|
+
for (const [key, value] of customAttrs) {
|
|
34
|
+
lines.push(`- \`${key}\` → \`${value}\``);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Link to Config Connector docs
|
|
38
|
+
const parts = entry.resourceType.split("::");
|
|
39
|
+
if (parts.length >= 3) {
|
|
40
|
+
const service = parts[1].toLowerCase();
|
|
41
|
+
const kind = parts[2].toLowerCase();
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push(`[Config Connector docs](https://cloud.google.com/config-connector/docs/reference/resource-docs/${service}/${service}${kind})`);
|
|
44
|
+
}
|
|
45
|
+
return { contents: lines.join("\n") };
|
|
46
|
+
}
|