@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 +21 -0
- package/README.md +45 -0
- package/bin/dotzen.js +3 -0
- package/dist/cli/check.d.ts +9 -0
- package/dist/cli/check.js +79 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +141 -0
- package/dist/cli/scaffold.d.ts +36 -0
- package/dist/cli/scaffold.js +171 -0
- package/dist/engine/evaluate.d.ts +31 -0
- package/dist/engine/evaluate.js +590 -0
- package/dist/hcl/model.d.ts +91 -0
- package/dist/hcl/model.js +5 -0
- package/dist/hcl/normalize.d.ts +16 -0
- package/dist/hcl/normalize.js +462 -0
- package/dist/hcl/parse.d.ts +13 -0
- package/dist/hcl/parse.js +85 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +37 -0
- package/dist/report/report.d.ts +18 -0
- package/dist/report/report.js +109 -0
- package/dist/result/errors.d.ts +27 -0
- package/dist/result/errors.js +3 -0
- package/dist/result/result.d.ts +22 -0
- package/dist/result/result.js +37 -0
- package/dist/spec/load.d.ts +14 -0
- package/dist/spec/load.js +78 -0
- package/dist/spec/rule.d.ts +161 -0
- package/dist/spec/rule.js +190 -0
- package/dist/version/config.d.ts +29 -0
- package/dist/version/config.js +66 -0
- package/dist/vocabulary/azure.d.ts +63 -0
- package/dist/vocabulary/azure.js +88 -0
- package/dist/vocabulary/gcp.d.ts +57 -0
- package/dist/vocabulary/gcp.js +80 -0
- package/dist/vocabulary/index.d.ts +174 -0
- package/dist/vocabulary/index.js +235 -0
- package/package.json +74 -0
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,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>;
|
package/dist/cli/main.js
ADDED
|
@@ -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;
|