nexarch 0.12.6 → 0.12.7
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/commands/ingest-infra.js +319 -0
- package/dist/commands/init-project-infra.js +170 -0
- package/dist/commands/init-project.js +19 -0
- package/dist/index.js +198 -192
- package/dist/lib/credentials.js +33 -1
- package/dist/lib/terraform-catalogue-seed.js +206 -0
- package/dist/lib/terraform-detect.js +338 -0
- package/dist/lib/terraform-projection.js +231 -0
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform state projector (ADR: infrastructure-as-code ingestion, v1).
|
|
3
|
+
*
|
|
4
|
+
* Turns `terraform show -json` into the small, safe projection that ingestion
|
|
5
|
+
* is allowed to send. This is the security-critical component of the feature:
|
|
6
|
+
* state files carry generated passwords, storage keys, connection strings and
|
|
7
|
+
* certificate material in clear text, verified against a real estate.
|
|
8
|
+
*
|
|
9
|
+
* Two independent gates, both deny-by-default:
|
|
10
|
+
*
|
|
11
|
+
* 1. The catalogue names, per resource type, the attributes that may be read.
|
|
12
|
+
* Anything not named is dropped. The catalogue is reference data served by
|
|
13
|
+
* the platform, not a list baked into this CLI, so support for a new Azure
|
|
14
|
+
* service is a data change rather than a release.
|
|
15
|
+
*
|
|
16
|
+
* 2. Terraform's own `sensitive_values` map is consulted for every attribute
|
|
17
|
+
* that survives gate 1, and anything flagged there is dropped even when the
|
|
18
|
+
* catalogue names it. A mistake in the curated list is therefore not on its
|
|
19
|
+
* own sufficient to leak a secret.
|
|
20
|
+
*
|
|
21
|
+
* Values are additionally restricted to scalars and shallow string arrays.
|
|
22
|
+
* Nested blocks are never emitted wholesale: a container app's `secret` block or
|
|
23
|
+
* an env var's `value` can hold credentials, so structure is only ever read by a
|
|
24
|
+
* named extractor that has been reviewed for that purpose.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Parses a state document from raw bytes.
|
|
28
|
+
*
|
|
29
|
+
* PowerShell's `>` redirection writes UTF-16 with a BOM, which is what a Windows
|
|
30
|
+
* user gets from `terraform show -json > state.json` and what a naive reader
|
|
31
|
+
* chokes on. Both endiannesses and plain UTF-8 are accepted.
|
|
32
|
+
*/
|
|
33
|
+
export function parseTerraformStateBuffer(buffer) {
|
|
34
|
+
let text;
|
|
35
|
+
if (buffer[0] === 0xff && buffer[1] === 0xfe)
|
|
36
|
+
text = buffer.toString("utf16le");
|
|
37
|
+
else if (buffer[0] === 0xfe && buffer[1] === 0xff)
|
|
38
|
+
text = Buffer.from(buffer).swap16().toString("utf16le");
|
|
39
|
+
else
|
|
40
|
+
text = buffer.toString("utf8");
|
|
41
|
+
return JSON.parse(text.replace(/^/, ""));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Infers the environment from resource tags.
|
|
45
|
+
*
|
|
46
|
+
* Estates that tag at all tag consistently — the first real one carried
|
|
47
|
+
* `environment` on every resource — so the dominant value of a conventional tag
|
|
48
|
+
* key beats asking a human to pass a flag correctly in a pipeline. Returns null
|
|
49
|
+
* when there is no clear winner, leaving the caller to fall back to the
|
|
50
|
+
* Terraform workspace or an explicit flag.
|
|
51
|
+
*/
|
|
52
|
+
export function inferEnvironmentFromTags(resources) {
|
|
53
|
+
const TAG_KEYS = ["environment", "env", "Environment", "Env"];
|
|
54
|
+
const counts = new Map();
|
|
55
|
+
for (const resource of resources) {
|
|
56
|
+
for (const key of TAG_KEYS) {
|
|
57
|
+
const value = resource.tags[key];
|
|
58
|
+
if (typeof value !== "string" || !value.trim())
|
|
59
|
+
continue;
|
|
60
|
+
const normalised = value.trim().toLowerCase();
|
|
61
|
+
counts.set(normalised, (counts.get(normalised) ?? 0) + 1);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (counts.size === 0)
|
|
66
|
+
return null;
|
|
67
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
68
|
+
const [environment, hits] = ranked[0];
|
|
69
|
+
const total = ranked.reduce((sum, [, n]) => sum + n, 0);
|
|
70
|
+
return { environment, confidence: hits / total };
|
|
71
|
+
}
|
|
72
|
+
/** Maps a free-text environment name onto an ontology environment subtype. */
|
|
73
|
+
export function environmentSubtypeFor(environment) {
|
|
74
|
+
const name = environment.trim().toLowerCase();
|
|
75
|
+
if (/^(prod|production|live)$/.test(name))
|
|
76
|
+
return "env_production";
|
|
77
|
+
if (/^(stag|staging|preprod|pre-prod|uat)$/.test(name))
|
|
78
|
+
return "env_staging";
|
|
79
|
+
if (/^(test|qa|sit)$/.test(name))
|
|
80
|
+
return "env_test";
|
|
81
|
+
if (/^(dr|failover)$/.test(name))
|
|
82
|
+
return "env_dr";
|
|
83
|
+
return "env_development";
|
|
84
|
+
}
|
|
85
|
+
/** Longest value we will carry for any single attribute. */
|
|
86
|
+
const MAX_VALUE_LENGTH = 400;
|
|
87
|
+
const MAX_ARRAY_ITEMS = 12;
|
|
88
|
+
function flattenResources(module, out = []) {
|
|
89
|
+
if (!module)
|
|
90
|
+
return out;
|
|
91
|
+
for (const resource of module.resources ?? [])
|
|
92
|
+
out.push(resource);
|
|
93
|
+
for (const child of module.child_modules ?? [])
|
|
94
|
+
flattenResources(child, out);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* True when Terraform flagged this attribute as sensitive. The map mirrors the
|
|
99
|
+
* shape of `values`, so a `true` anywhere beneath the attribute — an env var's
|
|
100
|
+
* value inside a container template, for instance — disqualifies the whole
|
|
101
|
+
* attribute rather than just the leaf.
|
|
102
|
+
*/
|
|
103
|
+
function isSensitive(sensitiveValues, attribute) {
|
|
104
|
+
if (!sensitiveValues || typeof sensitiveValues !== "object" || Array.isArray(sensitiveValues))
|
|
105
|
+
return false;
|
|
106
|
+
const flag = sensitiveValues[attribute];
|
|
107
|
+
if (flag === undefined)
|
|
108
|
+
return false;
|
|
109
|
+
if (flag === true)
|
|
110
|
+
return true;
|
|
111
|
+
return JSON.stringify(flag).includes("true");
|
|
112
|
+
}
|
|
113
|
+
/** Scalars and shallow string arrays only; anything structural is refused. */
|
|
114
|
+
function safeValue(value) {
|
|
115
|
+
if (value === null || value === undefined)
|
|
116
|
+
return null;
|
|
117
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
118
|
+
return value;
|
|
119
|
+
if (typeof value === "string")
|
|
120
|
+
return value.length > MAX_VALUE_LENGTH ? value.slice(0, MAX_VALUE_LENGTH) : value;
|
|
121
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
122
|
+
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => item.slice(0, MAX_VALUE_LENGTH));
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Tags carry the binding convention, so they are read — but a tag whose value
|
|
128
|
+
* Terraform flagged, or which does not look like a label, is left behind.
|
|
129
|
+
*/
|
|
130
|
+
function projectTags(values, sensitiveValues) {
|
|
131
|
+
if (isSensitive(sensitiveValues, "tags"))
|
|
132
|
+
return {};
|
|
133
|
+
const raw = values.tags;
|
|
134
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
135
|
+
return {};
|
|
136
|
+
const tags = {};
|
|
137
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
138
|
+
if (typeof value !== "string")
|
|
139
|
+
continue;
|
|
140
|
+
if (value.length > 200)
|
|
141
|
+
continue;
|
|
142
|
+
tags[key] = value;
|
|
143
|
+
}
|
|
144
|
+
return tags;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* User-assigned identity references. Reviewed extractor: identity ids are
|
|
148
|
+
* resource paths, never credentials, and they are how a workload is tied to the
|
|
149
|
+
* role assignments that define what it may reach.
|
|
150
|
+
*/
|
|
151
|
+
function projectIdentityIds(values) {
|
|
152
|
+
const identity = values.identity;
|
|
153
|
+
if (!Array.isArray(identity))
|
|
154
|
+
return [];
|
|
155
|
+
const ids = [];
|
|
156
|
+
for (const block of identity) {
|
|
157
|
+
if (!block || typeof block !== "object")
|
|
158
|
+
continue;
|
|
159
|
+
const identityIds = block.identity_ids;
|
|
160
|
+
if (!Array.isArray(identityIds))
|
|
161
|
+
continue;
|
|
162
|
+
for (const id of identityIds)
|
|
163
|
+
if (typeof id === "string")
|
|
164
|
+
ids.push(id);
|
|
165
|
+
}
|
|
166
|
+
return ids.slice(0, MAX_ARRAY_ITEMS);
|
|
167
|
+
}
|
|
168
|
+
export function projectTerraformState(params) {
|
|
169
|
+
const { document, catalogue, environment } = params;
|
|
170
|
+
const byType = new Map(catalogue.map((entry) => [entry.resourceType, entry]));
|
|
171
|
+
const resources = flattenResources(document.values?.root_module);
|
|
172
|
+
const projected = [];
|
|
173
|
+
const unknown = new Map();
|
|
174
|
+
let attributesDroppedNotAllowlisted = 0;
|
|
175
|
+
let attributesDroppedSensitive = 0;
|
|
176
|
+
for (const resource of resources) {
|
|
177
|
+
const resourceType = typeof resource.type === "string" ? resource.type : "";
|
|
178
|
+
if (!resourceType)
|
|
179
|
+
continue;
|
|
180
|
+
const entry = byType.get(resourceType);
|
|
181
|
+
if (!entry) {
|
|
182
|
+
unknown.set(resourceType, (unknown.get(resourceType) ?? 0) + 1);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const values = (resource.values && typeof resource.values === "object" ? resource.values : {});
|
|
186
|
+
const sensitiveValues = resource.sensitive_values;
|
|
187
|
+
const attributes = {};
|
|
188
|
+
for (const [key, value] of Object.entries(values)) {
|
|
189
|
+
if (!entry.attributes.includes(key)) {
|
|
190
|
+
attributesDroppedNotAllowlisted += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (isSensitive(sensitiveValues, key)) {
|
|
194
|
+
attributesDroppedSensitive += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const safe = safeValue(value);
|
|
198
|
+
if (safe === null) {
|
|
199
|
+
attributesDroppedNotAllowlisted += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
attributes[key] = safe;
|
|
203
|
+
}
|
|
204
|
+
projected.push({
|
|
205
|
+
address: typeof resource.address === "string" ? resource.address : `${resourceType}.${resource.name ?? "unknown"}`,
|
|
206
|
+
resourceType,
|
|
207
|
+
entityTypeCode: entry.entityTypeCode,
|
|
208
|
+
entitySubtypeCode: entry.entitySubtypeCode ?? null,
|
|
209
|
+
role: entry.role,
|
|
210
|
+
mode: typeof resource.mode === "string" ? resource.mode : "managed",
|
|
211
|
+
name: typeof values.name === "string" ? values.name : null,
|
|
212
|
+
attributes,
|
|
213
|
+
tags: projectTags(values, sensitiveValues),
|
|
214
|
+
identityIds: projectIdentityIds(values),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
environment,
|
|
219
|
+
terraformVersion: typeof document.terraform_version === "string" ? document.terraform_version : null,
|
|
220
|
+
resources: projected,
|
|
221
|
+
unknownTypes: [...unknown.entries()]
|
|
222
|
+
.map(([resourceType, count]) => ({ resourceType, count }))
|
|
223
|
+
.sort((a, b) => b.count - a.count),
|
|
224
|
+
stats: {
|
|
225
|
+
resourcesSeen: resources.length,
|
|
226
|
+
resourcesProjected: projected.length,
|
|
227
|
+
attributesDroppedNotAllowlisted,
|
|
228
|
+
attributesDroppedSensitive,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|