@dotzen/dotzen 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hcl/normalize.d.ts +4 -0
- package/dist/hcl/normalize.js +20 -2
- package/dist/hcl/parse.d.ts +3 -1
- package/dist/hcl/parse.js +87 -19
- package/package.json +74 -74
package/dist/hcl/normalize.d.ts
CHANGED
|
@@ -4,11 +4,15 @@ export interface Hcl2JsonRoot {
|
|
|
4
4
|
resource?: Record<string, Record<string, unknown[]>>;
|
|
5
5
|
variable?: Record<string, unknown[]>;
|
|
6
6
|
locals?: unknown[];
|
|
7
|
+
/** `module "x" { source = …, <inputs> }` → `{ x: [{ source, … }] }`. */
|
|
8
|
+
module?: Record<string, unknown[]>;
|
|
7
9
|
}
|
|
8
10
|
/** Resolved `var.*` / `local.*` values, keyed by reference, raw form. */
|
|
9
11
|
export type Scope = Map<string, unknown>;
|
|
10
12
|
/** Collect `variable` defaults and `locals` from all parsed files. */
|
|
11
13
|
export declare function buildScope(roots: Hcl2JsonRoot[]): Scope;
|
|
14
|
+
/** Follow a sole var/local reference to its raw resolved value (or undefined). */
|
|
15
|
+
export declare function resolveRaw(raw: unknown, scope: Scope, depth?: number): unknown | undefined;
|
|
12
16
|
/**
|
|
13
17
|
* Adapter boundary (doc 06): parser output -> dotzen's own model.
|
|
14
18
|
* The engine never sees `Hcl2JsonRoot`. `scope` resolves var/local refs.
|
package/dist/hcl/normalize.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildScope = buildScope;
|
|
4
|
+
exports.resolveRaw = resolveRaw;
|
|
4
5
|
exports.normalize = normalize;
|
|
5
6
|
const vocabulary_1 = require("../vocabulary");
|
|
6
7
|
const KNOWN_TYPES = new Set([
|
|
@@ -60,11 +61,28 @@ function buildScope(roots) {
|
|
|
60
61
|
}
|
|
61
62
|
function mapIngressObj(o, scope) {
|
|
62
63
|
const oo = asObject(o);
|
|
63
|
-
const
|
|
64
|
+
const raw = oo.cidr_blocks;
|
|
65
|
+
let cidrBlocks;
|
|
66
|
+
if (Array.isArray(raw)) {
|
|
67
|
+
cidrBlocks = raw.map((c) => resolveValue(c, scope));
|
|
68
|
+
}
|
|
69
|
+
else if (typeof raw === 'string') {
|
|
70
|
+
// A whole-list reference, e.g. `cidr_blocks = var.allowed_cidrs`
|
|
71
|
+
// (common in modules). Follow it to a concrete list; if it can't be
|
|
72
|
+
// resolved, keep it as one unresolved element so the check honestly
|
|
73
|
+
// degrades to "could not evaluate" instead of silently passing.
|
|
74
|
+
const resolved = resolveRaw(raw, scope);
|
|
75
|
+
cidrBlocks = Array.isArray(resolved)
|
|
76
|
+
? resolved.map((c) => resolveValue(c, scope))
|
|
77
|
+
: [{ kind: 'unresolved', expr: raw }];
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
cidrBlocks = [];
|
|
81
|
+
}
|
|
64
82
|
return {
|
|
65
83
|
fromPort: resolveValue(oo.from_port, scope),
|
|
66
84
|
toPort: resolveValue(oo.to_port, scope),
|
|
67
|
-
cidrBlocks
|
|
85
|
+
cidrBlocks,
|
|
68
86
|
};
|
|
69
87
|
}
|
|
70
88
|
/** Inline `<name> { ... }` blocks (name is 'ingress' or 'egress'). */
|
package/dist/hcl/parse.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { NormalizedResource } from './model';
|
|
|
8
8
|
* `projectRoot` (defaults to `dir`) so output is readable and portable —
|
|
9
9
|
* and, for multi-root layouts, shows which root each finding came from.
|
|
10
10
|
* A single `parseTf` call builds ONE scope, so calling it once per root
|
|
11
|
-
* keeps each root's `var`/`local` values isolated.
|
|
11
|
+
* keeps each root's `var`/`local` values isolated. Local `module {}` calls
|
|
12
|
+
* are followed (doc 08), evaluating the module's resources with the
|
|
13
|
+
* caller's inputs threaded in as `var.*`.
|
|
12
14
|
*/
|
|
13
15
|
export declare function parseTf(dir: string, projectRoot?: string, environmentOverride?: string): Promise<Result<NormalizedResource[], DotzenError>>;
|
package/dist/hcl/parse.js
CHANGED
|
@@ -44,27 +44,16 @@ function findTfFiles(dir) {
|
|
|
44
44
|
return entries.filter((e) => e.endsWith('.tf')).map((e) => path.join(dir, e));
|
|
45
45
|
}
|
|
46
46
|
const toPosix = (p) => p.split(path.sep).join('/');
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
* (hcl2json / WASM), and normalize into dotzen's model. Async because
|
|
50
|
-
* the WASM parser is async. Reported file paths are made relative to
|
|
51
|
-
* `projectRoot` (defaults to `dir`) so output is readable and portable —
|
|
52
|
-
* and, for multi-root layouts, shows which root each finding came from.
|
|
53
|
-
* A single `parseTf` call builds ONE scope, so calling it once per root
|
|
54
|
-
* keeps each root's `var`/`local` values isolated.
|
|
55
|
-
*/
|
|
56
|
-
async function parseTf(dir, projectRoot = dir, environmentOverride) {
|
|
47
|
+
/** Parse every `.tf` under `dir` (no normalization). */
|
|
48
|
+
async function parseDir(dir) {
|
|
57
49
|
if (!fs.existsSync(dir))
|
|
58
50
|
return (0, result_1.err)({ kind: 'PathNotFound', path: dir });
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
// file from the resources that reference them).
|
|
62
|
-
const parsedFiles = [];
|
|
63
|
-
for (const file of files) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const file of findTfFiles(dir)) {
|
|
64
53
|
const text = fs.readFileSync(file, 'utf8');
|
|
65
54
|
try {
|
|
66
55
|
const parsed = (await (0, hcl2json_1.parse)(path.basename(file), text));
|
|
67
|
-
|
|
56
|
+
out.push({ file, text, parsed });
|
|
68
57
|
}
|
|
69
58
|
catch (e) {
|
|
70
59
|
return (0, result_1.err)({
|
|
@@ -74,12 +63,91 @@ async function parseTf(dir, projectRoot = dir, environmentOverride) {
|
|
|
74
63
|
});
|
|
75
64
|
}
|
|
76
65
|
}
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
return (0, result_1.ok)(out);
|
|
67
|
+
}
|
|
68
|
+
// Keys on a `module` block that are meta, not `var` inputs (doc 08).
|
|
69
|
+
const MODULE_META = new Set([
|
|
70
|
+
'source',
|
|
71
|
+
'version',
|
|
72
|
+
'count',
|
|
73
|
+
'for_each',
|
|
74
|
+
'providers',
|
|
75
|
+
'depends_on',
|
|
76
|
+
'lifecycle',
|
|
77
|
+
]);
|
|
78
|
+
// A local relative source we can follow (doc 08 v1: local sources only).
|
|
79
|
+
const isLocalSource = (s) => typeof s === 'string' && (s.startsWith('./') || s.startsWith('../'));
|
|
80
|
+
/**
|
|
81
|
+
* Module-following (doc 08). For each local `module { source, <inputs> }`
|
|
82
|
+
* call in the caller's files, parse the module dir and normalize its
|
|
83
|
+
* resources with a scope = the module's own defaults/locals overlaid with
|
|
84
|
+
* the caller's inputs (resolved in the caller's scope) as `var.*`. This is
|
|
85
|
+
* what turns caller-`var` values (cidrs, tags) into concrete verdicts.
|
|
86
|
+
* v1: local sources, single level, no count/for_each.
|
|
87
|
+
*/
|
|
88
|
+
async function followModules(callerFiles, rootDir, projectRoot, callerScope, environmentOverride) {
|
|
89
|
+
const rootRel = toPosix(path.relative(projectRoot, rootDir));
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const { file, parsed } of callerFiles) {
|
|
92
|
+
for (const [, calls] of Object.entries(parsed.module ?? {})) {
|
|
93
|
+
for (const raw of Array.isArray(calls) ? calls : []) {
|
|
94
|
+
const block = (raw ?? {});
|
|
95
|
+
if (!isLocalSource(block.source))
|
|
96
|
+
continue; // remote → not followed
|
|
97
|
+
const moduleDir = path.resolve(path.dirname(file), block.source);
|
|
98
|
+
// Confine to the scanned project; never escape it.
|
|
99
|
+
if (path.relative(projectRoot, moduleDir).startsWith('..'))
|
|
100
|
+
continue;
|
|
101
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- moduleDir confined to projectRoot above
|
|
102
|
+
if (!fs.existsSync(moduleDir))
|
|
103
|
+
continue;
|
|
104
|
+
const parsedModule = await parseDir(moduleDir);
|
|
105
|
+
if (!parsedModule.ok)
|
|
106
|
+
return parsedModule;
|
|
107
|
+
// Module scope: its own defaults/locals, then caller inputs win.
|
|
108
|
+
const moduleScope = (0, normalize_1.buildScope)(parsedModule.value.map((f) => f.parsed));
|
|
109
|
+
for (const [name, value] of Object.entries(block)) {
|
|
110
|
+
if (MODULE_META.has(name))
|
|
111
|
+
continue;
|
|
112
|
+
const resolved = (0, normalize_1.resolveRaw)(value, callerScope);
|
|
113
|
+
moduleScope.set(`var.${name}`, resolved !== undefined ? resolved : value);
|
|
114
|
+
}
|
|
115
|
+
for (const m of parsedModule.value) {
|
|
116
|
+
const modRel = toPosix(path.relative(projectRoot, m.file));
|
|
117
|
+
const trace = rootRel ? `${rootRel} › ${modRel}` : modRel;
|
|
118
|
+
out.push(...(0, normalize_1.normalize)(m.parsed, trace, m.text, moduleScope, environmentOverride));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return (0, result_1.ok)(out);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Read a terraform directory, parse each .tf via the official parser
|
|
127
|
+
* (hcl2json / WASM), and normalize into dotzen's model. Async because
|
|
128
|
+
* the WASM parser is async. Reported file paths are made relative to
|
|
129
|
+
* `projectRoot` (defaults to `dir`) so output is readable and portable —
|
|
130
|
+
* and, for multi-root layouts, shows which root each finding came from.
|
|
131
|
+
* A single `parseTf` call builds ONE scope, so calling it once per root
|
|
132
|
+
* keeps each root's `var`/`local` values isolated. Local `module {}` calls
|
|
133
|
+
* are followed (doc 08), evaluating the module's resources with the
|
|
134
|
+
* caller's inputs threaded in as `var.*`.
|
|
135
|
+
*/
|
|
136
|
+
async function parseTf(dir, projectRoot = dir, environmentOverride) {
|
|
137
|
+
const parsedFiles = await parseDir(dir);
|
|
138
|
+
if (!parsedFiles.ok)
|
|
139
|
+
return parsedFiles;
|
|
140
|
+
// Build the cross-file var/local scope, then normalize direct resources.
|
|
141
|
+
const scope = (0, normalize_1.buildScope)(parsedFiles.value.map((p) => p.parsed));
|
|
79
142
|
const resources = [];
|
|
80
|
-
for (const { file, text, parsed } of parsedFiles) {
|
|
143
|
+
for (const { file, text, parsed } of parsedFiles.value) {
|
|
81
144
|
const rel = toPosix(path.relative(projectRoot, file));
|
|
82
145
|
resources.push(...(0, normalize_1.normalize)(parsed, rel, text, scope, environmentOverride));
|
|
83
146
|
}
|
|
147
|
+
// Follow local module calls, threading caller inputs into their vars.
|
|
148
|
+
const followed = await followModules(parsedFiles.value, dir, projectRoot, scope, environmentOverride);
|
|
149
|
+
if (!followed.ok)
|
|
150
|
+
return followed;
|
|
151
|
+
resources.push(...followed.value);
|
|
84
152
|
return (0, result_1.ok)(resources);
|
|
85
153
|
}
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dotzen/dotzen",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Prose as Code — governance for AI-generated Terraform (AWS, Azure, GCP).",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"terraform",
|
|
8
|
-
"hcl",
|
|
9
|
-
"governance",
|
|
10
|
-
"policy-as-code",
|
|
11
|
-
"iac",
|
|
12
|
-
"security",
|
|
13
|
-
"cis",
|
|
14
|
-
"compliance",
|
|
15
|
-
"aws",
|
|
16
|
-
"azure",
|
|
17
|
-
"gcp"
|
|
18
|
-
],
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://gitlab.com/governance-tools/dotzen.git",
|
|
22
|
-
"directory": "packages/cli"
|
|
23
|
-
},
|
|
24
|
-
"homepage": "https://gitlab.com/governance-tools/dotzen#readme",
|
|
25
|
-
"bugs": {
|
|
26
|
-
"url": "https://gitlab.com/governance-tools/dotzen/-/issues"
|
|
27
|
-
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"dotzen": "./bin/dotzen.js"
|
|
30
|
-
},
|
|
31
|
-
"main": "./dist/index.js",
|
|
32
|
-
"types": "./dist/index.d.ts",
|
|
33
|
-
"exports": {
|
|
34
|
-
".": {
|
|
35
|
-
"types": "./dist/index.d.ts",
|
|
36
|
-
"default": "./dist/index.js"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"dist",
|
|
41
|
-
"bin"
|
|
42
|
-
],
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=18"
|
|
45
|
-
},
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public"
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsc -p tsconfig.build.json",
|
|
51
|
-
"typecheck": "tsc --noEmit",
|
|
52
|
-
"test": "vitest run src",
|
|
53
|
-
"test:integration": "vitest run tests",
|
|
54
|
-
"coverage": "vitest run --coverage",
|
|
55
|
-
"lint": "eslint .",
|
|
56
|
-
"format:check": "prettier --check .",
|
|
57
|
-
"format": "prettier --write .",
|
|
58
|
-
"prepublishOnly": "npm run build"
|
|
59
|
-
},
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@cdktf/hcl2json": "^0.21.0",
|
|
62
|
-
"jiti": "^2.4.2"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@types/node": "^22.10.5",
|
|
66
|
-
"@vitest/coverage-v8": "^4.1.10",
|
|
67
|
-
"eslint": "^9.17.0",
|
|
68
|
-
"eslint-plugin-security": "^3.0.1",
|
|
69
|
-
"prettier": "^3.4.2",
|
|
70
|
-
"typescript": "^5.7.2",
|
|
71
|
-
"typescript-eslint": "^8.19.0",
|
|
72
|
-
"vitest": "^4.1.10"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotzen/dotzen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prose as Code — governance for AI-generated Terraform (AWS, Azure, GCP).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"terraform",
|
|
8
|
+
"hcl",
|
|
9
|
+
"governance",
|
|
10
|
+
"policy-as-code",
|
|
11
|
+
"iac",
|
|
12
|
+
"security",
|
|
13
|
+
"cis",
|
|
14
|
+
"compliance",
|
|
15
|
+
"aws",
|
|
16
|
+
"azure",
|
|
17
|
+
"gcp"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://gitlab.com/governance-tools/dotzen.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://gitlab.com/governance-tools/dotzen#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://gitlab.com/governance-tools/dotzen/-/issues"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"dotzen": "./bin/dotzen.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"bin"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run src",
|
|
53
|
+
"test:integration": "vitest run tests",
|
|
54
|
+
"coverage": "vitest run --coverage",
|
|
55
|
+
"lint": "eslint .",
|
|
56
|
+
"format:check": "prettier --check .",
|
|
57
|
+
"format": "prettier --write .",
|
|
58
|
+
"prepublishOnly": "npm run build"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@cdktf/hcl2json": "^0.21.0",
|
|
62
|
+
"jiti": "^2.4.2"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^22.10.5",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
67
|
+
"eslint": "^9.17.0",
|
|
68
|
+
"eslint-plugin-security": "^3.0.1",
|
|
69
|
+
"prettier": "^3.4.2",
|
|
70
|
+
"typescript": "^5.7.2",
|
|
71
|
+
"typescript-eslint": "^8.19.0",
|
|
72
|
+
"vitest": "^4.1.10"
|
|
73
|
+
}
|
|
74
|
+
}
|