@dotzen/dotzen 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Machado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @dotzen/dotzen
2
+
3
+ **Prose as Code.** Zero-install governance for AI-generated Terraform — across **AWS, Azure, and GCP**.
4
+
5
+ ```bash
6
+ npx @dotzen/dotzen check ./terraform/
7
+ ```
8
+
9
+ dotzen catches security, tagging, and compliance violations in Terraform HCL — especially the kind AI code-generation tools produce when they don't know your organization's policies. Rules are written in a readable, strongly-typed TypeScript DSL (`.zen/spec.ts`) meant to be reviewable by a security architect who has never written code:
10
+
11
+ ```ts
12
+ import { rule, AwsResource, Port } from '@dotzen/dotzen'
13
+
14
+ export const spec = [
15
+ rule()
16
+ .resource(AwsResource.SecurityGroup)
17
+ .denyIngress(Port.SSH, Port.RDP)
18
+ .message('SSH and RDP must not be open to the internet')
19
+ .rationale('CIS AWS Foundations Benchmark, control 5.2'),
20
+ ]
21
+ ```
22
+
23
+ Each finding is `block` (fails the build), `warn`, or `require_approval` (pauses CI for sign-off). When a value can't be resolved statically, dotzen reports **"could not evaluate"** rather than guessing — a false positive is worse than an honest gap.
24
+
25
+ ## Getting started
26
+
27
+ ```bash
28
+ npx @dotzen/dotzen init # scaffold .zen/spec.ts + dotzen.json
29
+ npx @dotzen/dotzen check # evaluate ./terraform against the spec
30
+ ```
31
+
32
+ - `--format json` for machine-readable output.
33
+ - Pin the version in `dotzen.json` (never `@latest` in CI).
34
+
35
+ ## Coverage
36
+
37
+ Three clouds, one engine: **AWS** (deep), **Azure** and **GCP** at ~CIS Foundations Level 1 — network exposure, encryption at rest/in transit, public access, IAM/RBAC over-permission, audit logging, and hardcoded secrets.
38
+
39
+ ## Docs
40
+
41
+ Full documentation, design rationale, and the roadmap live in the [project repository](https://gitlab.com/governance-tools/dotzen). The parser is the official `hashicorp/hcl` compiled to WASM (`@cdktf/hcl2json`) — pure JS, no native binary.
42
+
43
+ ## License
44
+
45
+ MIT © Eduardo Machado
package/bin/dotzen.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Thin entry point (doc 03). Execs the compiled engine; no native binary.
3
+ require('../dist/cli/main.js')
@@ -0,0 +1,9 @@
1
+ import { Result } from '../result/result';
2
+ import { DotzenError } from '../result/errors';
3
+ import { CheckReport } from '../engine/evaluate';
4
+ /**
5
+ * The pipeline (doc 06). Railway: every operational stage short-circuits
6
+ * on error. `evaluate` is total, so it is the final `.map`-style step,
7
+ * not a fallible stage. Written imperatively because stages are async.
8
+ */
9
+ export declare function check(projectRoot: string, engineVersion: string): Promise<Result<CheckReport, DotzenError>>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.check = check;
37
+ const path = __importStar(require("path"));
38
+ const result_1 = require("../result/result");
39
+ const config_1 = require("../version/config");
40
+ const load_1 = require("../spec/load");
41
+ const parse_1 = require("../hcl/parse");
42
+ const evaluate_1 = require("../engine/evaluate");
43
+ /**
44
+ * The pipeline (doc 06). Railway: every operational stage short-circuits
45
+ * on error. `evaluate` is total, so it is the final `.map`-style step,
46
+ * not a fallible stage. Written imperatively because stages are async.
47
+ */
48
+ async function check(projectRoot, engineVersion) {
49
+ const loaded = (0, config_1.readDotzenJson)(projectRoot);
50
+ if (!loaded.ok)
51
+ return loaded;
52
+ const versioned = (0, config_1.enforceVersion)(loaded.value.config, engineVersion);
53
+ if (!versioned.ok)
54
+ return versioned;
55
+ const { baseDir } = loaded.value;
56
+ const { spec, terraform } = loaded.value.config;
57
+ const builders = await (0, load_1.importSpecModule)(path.resolve(baseDir, spec));
58
+ if (!builders.ok)
59
+ return builders;
60
+ const rules = (0, load_1.loadSpec)(builders.value);
61
+ if (!rules.ok)
62
+ return rules;
63
+ // Each root is a separate Terraform module: parse it independently so its
64
+ // var/local scope stays isolated (no cross-root collisions). File paths
65
+ // are reported relative to the project root, so findings show their root.
66
+ // A root may declare an `environment`, which drives `.environment(X)`
67
+ // rule scoping by folder instead of by tag.
68
+ const roots = Array.isArray(terraform) ? terraform : [terraform];
69
+ const resources = [];
70
+ for (const root of roots) {
71
+ const rootPath = typeof root === 'string' ? root : root.path;
72
+ const env = typeof root === 'string' ? undefined : root.environment;
73
+ const parsed = await (0, parse_1.parseTf)(path.resolve(baseDir, rootPath), baseDir, env);
74
+ if (!parsed.ok)
75
+ return parsed;
76
+ resources.push(...parsed.value);
77
+ }
78
+ return (0, result_1.ok)((0, evaluate_1.evaluate)(rules.value, resources));
79
+ }
@@ -0,0 +1 @@
1
+ export declare function run(argv: string[]): Promise<number>;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.run = run;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const check_1 = require("./check");
40
+ const report_1 = require("../report/report");
41
+ const scaffold_1 = require("./scaffold");
42
+ /**
43
+ * Emit the approval signal for CI (doc 04), so a later manual-approval job
44
+ * can gate on DOTZEN_REQUIRES_APPROVAL. CI-agnostic:
45
+ * - GitLab CI (and any CI): write a dotenv file the pipeline exposes via
46
+ * `artifacts:reports:dotenv` (path overridable with DOTZEN_ENV_FILE,
47
+ * default `dotzen.env`).
48
+ * - GitHub Actions: also append to $GITHUB_ENV if present.
49
+ * No-op outside CI, so local runs never leave a stray file.
50
+ */
51
+ function emitApprovalSignal(report) {
52
+ const line = `DOTZEN_REQUIRES_APPROVAL=${(0, report_1.requiresApproval)(report)}\n`;
53
+ const ghEnv = process.env.GITHUB_ENV;
54
+ if (ghEnv)
55
+ fs.appendFileSync(ghEnv, line);
56
+ if (process.env.GITLAB_CI || process.env.CI) {
57
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CI-controlled dotenv path
58
+ fs.writeFileSync(process.env.DOTZEN_ENV_FILE ?? 'dotzen.env', line);
59
+ }
60
+ }
61
+ function engineVersion() {
62
+ const pkg = path.join(__dirname, '..', '..', 'package.json');
63
+ return JSON.parse(fs.readFileSync(pkg, 'utf8'))
64
+ .version;
65
+ }
66
+ function parseArgs(argv) {
67
+ const [command, ...rest] = argv;
68
+ let root = '.';
69
+ let json = false;
70
+ let terraform;
71
+ for (let i = 0; i < rest.length; i++) {
72
+ const a = rest[i];
73
+ if (a === '--format') {
74
+ json = rest[i + 1] === 'json';
75
+ i++;
76
+ }
77
+ else if (a === '--format=json') {
78
+ json = true;
79
+ }
80
+ else if (a === '--terraform') {
81
+ terraform = rest[i + 1];
82
+ i++;
83
+ }
84
+ else if (a?.startsWith('--terraform=')) {
85
+ terraform = a.slice('--terraform='.length);
86
+ }
87
+ else if (a && !a.startsWith('--')) {
88
+ root = a;
89
+ }
90
+ }
91
+ return { command, root, json, terraform };
92
+ }
93
+ function runInit(dir, terraform) {
94
+ const res = (0, scaffold_1.initProject)(dir, engineVersion(), { terraform });
95
+ for (const c of res.created)
96
+ process.stdout.write(` created ${c}\n`);
97
+ for (const s of res.skipped)
98
+ process.stdout.write(` skipped ${s} (already exists)\n`);
99
+ if (res.detected) {
100
+ const roots = Array.isArray(res.terraform) ? res.terraform : [res.terraform];
101
+ const fmt = (r) => typeof r === 'string' ? `"${r}"` : `"${r.path}" (${r.environment})`;
102
+ const label = roots.length > 1
103
+ ? `${roots.length} Terraform roots: ${roots.map(fmt).join(', ')}`
104
+ : `existing Terraform at ${fmt(roots[0])}`;
105
+ process.stdout.write(`\nUsing ${label} (from dotzen.json).\n` +
106
+ `If that's not right, edit "terraform" in dotzen.json (or re-run with --terraform <path>).\n` +
107
+ `Then run: npx @dotzen/dotzen check\n`);
108
+ }
109
+ else {
110
+ process.stdout.write('\nNext: add .tf files under terraform/, then run: npx @dotzen/dotzen check\n');
111
+ }
112
+ return 0;
113
+ }
114
+ async function run(argv) {
115
+ const { command, root, json, terraform } = parseArgs(argv);
116
+ if (command === 'init')
117
+ return runInit(root, terraform);
118
+ if (command !== 'check') {
119
+ process.stderr.write('usage: dotzen <check|init> [projectRoot] [--format json]\n');
120
+ return 2;
121
+ }
122
+ const result = await (0, check_1.check)(root, engineVersion());
123
+ if (!result.ok) {
124
+ process.stderr.write((0, report_1.renderError)(result.error) + '\n');
125
+ return 2;
126
+ }
127
+ // Color only a real terminal; honor NO_COLOR. Never color JSON or logs.
128
+ const color = process.stdout.isTTY === true && !process.env.NO_COLOR;
129
+ const output = json
130
+ ? (0, report_1.renderJson)(result.value)
131
+ : (0, report_1.renderTerminal)(result.value, { color });
132
+ process.stdout.write(output + '\n');
133
+ emitApprovalSignal(result.value);
134
+ return (0, report_1.reportExitCode)(result.value);
135
+ }
136
+ run(process.argv.slice(2))
137
+ .then((code) => process.exit(code))
138
+ .catch((e) => {
139
+ process.stderr.write(`✗ unexpected error: ${String(e)}\n`);
140
+ process.exit(2);
141
+ });
@@ -0,0 +1,36 @@
1
+ import { TerraformRoot } from '../version/config';
2
+ export interface ScaffoldFile {
3
+ readonly path: string;
4
+ readonly content: string;
5
+ }
6
+ /** The files `dotzen init` writes (pure — no filesystem access). */
7
+ export declare function scaffoldFiles(version: string, terraform?: TerraformRoot | TerraformRoot[]): ScaffoldFile[];
8
+ /**
9
+ * Every directory (relative to `dir`) that contains `.tf` files *directly* —
10
+ * i.e. every Terraform root module. `env/{dev,stg,prd}` yields three.
11
+ */
12
+ export declare function tfRootDirs(dir: string): string[];
13
+ /**
14
+ * Detect where a project's existing Terraform lives, so init points
15
+ * `dotzen.json` at the real path(s) instead of a fresh empty `terraform/`.
16
+ * Returns a single path, or an array of roots (multiple, e.g.
17
+ * per-environment) — mapping recognizable env folder names to an
18
+ * `environment` so `.environment(X)` scoping works by folder. Returns
19
+ * undefined for a greenfield project (no .tf yet).
20
+ */
21
+ export declare function detectTerraform(dir: string): TerraformRoot | TerraformRoot[] | undefined;
22
+ export interface InitResult {
23
+ readonly created: string[];
24
+ readonly skipped: string[];
25
+ readonly terraform: TerraformRoot | TerraformRoot[];
26
+ readonly detected: boolean;
27
+ }
28
+ /**
29
+ * Scaffold a new dotzen project into `dir`. Never overwrites an existing
30
+ * file (fail-safe). Adapts `terraform` to an existing layout: an explicit
31
+ * `opts.terraform` wins; otherwise it is auto-detected; a greenfield
32
+ * project falls back to `./terraform` (and that dir is created).
33
+ */
34
+ export declare function initProject(dir: string, version: string, opts?: {
35
+ terraform?: TerraformRoot | TerraformRoot[];
36
+ }): InitResult;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.scaffoldFiles = scaffoldFiles;
37
+ exports.tfRootDirs = tfRootDirs;
38
+ exports.detectTerraform = detectTerraform;
39
+ exports.initProject = initProject;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const vocabulary_1 = require("../vocabulary");
43
+ function dotzenJson(version, terraform) {
44
+ return (JSON.stringify({ version, spec: '.zen/spec.ts', terraform }, null, 2) + '\n');
45
+ }
46
+ function specTs() {
47
+ return `import { rule, AwsResource, Port, Tag, AwsAttribute, Acl } from '@dotzen/dotzen'
48
+
49
+ // Prose as Code: each rule reads like a policy statement. Autocomplete
50
+ // guides every choice, and a typo is a compile error, not a silent gap.
51
+ export const spec = [
52
+ rule()
53
+ .resource(AwsResource.SecurityGroup)
54
+ .denyIngress(Port.SSH, Port.RDP)
55
+ .message('SSH and RDP must not be open to the internet')
56
+ .rationale('CIS AWS Foundations Benchmark v1.4, control 5.2'),
57
+
58
+ rule()
59
+ .resource(
60
+ AwsResource.SecurityGroup,
61
+ AwsResource.DbInstance,
62
+ AwsResource.S3Bucket,
63
+ )
64
+ .mustHaveTags(Tag.Team, Tag.CostCenter, Tag.Environment)
65
+ .message('Required tags missing: team, cost_center, environment')
66
+ .rationale('FinOps ownership + cost allocation policy'),
67
+
68
+ rule()
69
+ .resource(AwsResource.DbInstance)
70
+ .mustBeTrue(AwsAttribute.StorageEncrypted)
71
+ .message('RDS instances must have storage encryption at rest'),
72
+
73
+ rule()
74
+ .resource(AwsResource.S3Bucket)
75
+ .denyAcl(Acl.PublicRead, Acl.PublicReadWrite)
76
+ .message('S3 buckets must not have a public ACL'),
77
+ ]
78
+ `;
79
+ }
80
+ /** The files `dotzen init` writes (pure — no filesystem access). */
81
+ function scaffoldFiles(version, terraform = './terraform') {
82
+ return [
83
+ { path: 'dotzen.json', content: dotzenJson(version, terraform) },
84
+ { path: path.join('.zen', 'spec.ts'), content: specTs() },
85
+ ];
86
+ }
87
+ const ignored = (rel) => rel.split(/[\\/]/).some((p) => p.startsWith('.') || p === 'node_modules');
88
+ /**
89
+ * Every directory (relative to `dir`) that contains `.tf` files *directly* —
90
+ * i.e. every Terraform root module. `env/{dev,stg,prd}` yields three.
91
+ */
92
+ function tfRootDirs(dir) {
93
+ if (!fs.existsSync(dir))
94
+ return [];
95
+ const entries = fs.readdirSync(dir, { recursive: true });
96
+ const roots = new Set();
97
+ for (const e of entries) {
98
+ if (!e.endsWith('.tf') || ignored(e))
99
+ continue;
100
+ const rel = path.dirname(e);
101
+ roots.add(rel === '.' ? '.' : './' + rel.split(/[\\/]/).join('/'));
102
+ }
103
+ return [...roots].sort();
104
+ }
105
+ // Guess a dotzen Environment from a root folder's leaf name (best-effort;
106
+ // the author edits/removes what doesn't fit). Folder names are arbitrary —
107
+ // only the mapped value must be a valid Environment.
108
+ const ENV_GUESS = {
109
+ dev: vocabulary_1.Environment.Development,
110
+ development: vocabulary_1.Environment.Development,
111
+ sandbox: vocabulary_1.Environment.Development,
112
+ stg: vocabulary_1.Environment.Staging,
113
+ stage: vocabulary_1.Environment.Staging,
114
+ staging: vocabulary_1.Environment.Staging,
115
+ prd: vocabulary_1.Environment.Production,
116
+ prod: vocabulary_1.Environment.Production,
117
+ production: vocabulary_1.Environment.Production,
118
+ };
119
+ const withEnvGuess = (rootPath) => {
120
+ const leaf = (rootPath.split('/').pop() ?? '').toLowerCase();
121
+ const environment = ENV_GUESS[leaf];
122
+ return environment ? { path: rootPath, environment } : rootPath;
123
+ };
124
+ /**
125
+ * Detect where a project's existing Terraform lives, so init points
126
+ * `dotzen.json` at the real path(s) instead of a fresh empty `terraform/`.
127
+ * Returns a single path, or an array of roots (multiple, e.g.
128
+ * per-environment) — mapping recognizable env folder names to an
129
+ * `environment` so `.environment(X)` scoping works by folder. Returns
130
+ * undefined for a greenfield project (no .tf yet).
131
+ */
132
+ function detectTerraform(dir) {
133
+ const roots = tfRootDirs(dir);
134
+ if (roots.length === 0)
135
+ return undefined;
136
+ if (roots.length === 1)
137
+ return roots[0];
138
+ return roots.map(withEnvGuess);
139
+ }
140
+ /**
141
+ * Scaffold a new dotzen project into `dir`. Never overwrites an existing
142
+ * file (fail-safe). Adapts `terraform` to an existing layout: an explicit
143
+ * `opts.terraform` wins; otherwise it is auto-detected; a greenfield
144
+ * project falls back to `./terraform` (and that dir is created).
145
+ */
146
+ function initProject(dir, version, opts = {}) {
147
+ const detected = opts.terraform ?? detectTerraform(dir);
148
+ const terraform = detected ?? './terraform';
149
+ const greenfield = detected === undefined;
150
+ const created = [];
151
+ const skipped = [];
152
+ for (const f of scaffoldFiles(version, terraform)) {
153
+ const target = path.join(dir, f.path);
154
+ if (fs.existsSync(target)) {
155
+ skipped.push(f.path);
156
+ continue;
157
+ }
158
+ fs.mkdirSync(path.dirname(target), { recursive: true });
159
+ fs.writeFileSync(target, f.content);
160
+ created.push(f.path);
161
+ }
162
+ // Only scaffold an empty terraform/ dir for a greenfield project.
163
+ if (greenfield) {
164
+ const tf = path.join(dir, 'terraform');
165
+ if (!fs.existsSync(tf)) {
166
+ fs.mkdirSync(tf, { recursive: true });
167
+ created.push('terraform/');
168
+ }
169
+ }
170
+ return { created, skipped, terraform, detected: !greenfield };
171
+ }
@@ -0,0 +1,31 @@
1
+ import { Rule } from '../spec/rule';
2
+ import { NormalizedResource } from '../hcl/model';
3
+ import { Effect } from '../vocabulary';
4
+ export interface Violation {
5
+ readonly ruleId: string;
6
+ readonly message: string;
7
+ readonly rationale?: string;
8
+ readonly effect: Effect;
9
+ readonly resource: string;
10
+ readonly file: string;
11
+ readonly line: number;
12
+ readonly approvers?: string[];
13
+ }
14
+ export interface Unevaluable {
15
+ readonly ruleId: string;
16
+ readonly resource: string;
17
+ readonly file: string;
18
+ readonly line: number;
19
+ readonly reason: string;
20
+ }
21
+ /** Success-track payload with THREE outcomes (doc 06, Rule 2). */
22
+ export interface CheckReport {
23
+ readonly violations: Violation[];
24
+ readonly passed: number;
25
+ readonly couldNotEvaluate: Unevaluable[];
26
+ }
27
+ /**
28
+ * TOTAL: always returns a CheckReport (doc 06, Rule 4). Folds over
29
+ * rules x conditions x in-scope resources (Rule 3), accumulating.
30
+ */
31
+ export declare function evaluate(rules: Rule[], resources: NormalizedResource[]): CheckReport;