diff --git a/README.ja.md b/README.ja.md index ad1e85d..a04065f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -12,6 +12,8 @@ > **「CDN セキュリティを“設計思想ごと”再利用可能にし、 > 世界中の誰でも短時間で安全な初期構成を作れるようにする」** +**最初に推奨する導入ルート:** `npx cdn-security init --platform aws --archetype spa-static-site --force` から始め、生成された policy を build し、AWS CloudFront Function と WAF Terraform 出力を既存 IaC に組み込みます。Cloudflare Workers も対応していますが、現時点で最初の本番導入パスとして最も揃っているのは AWS + Terraform です。 + --- ## なぜこのフレームワークが必要か diff --git a/README.md b/README.md index 5004131..bb8fcfd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The goal is simple. > **"Make CDN security reusable as a design philosophy, so anyone in the world can build a secure initial setup in a short time."** +**Recommended first path:** start with `npx cdn-security init --platform aws --archetype spa-static-site --force`, build the generated policy, then wire the AWS CloudFront Function and WAF Terraform outputs into your existing infrastructure. Cloudflare Workers is also supported, but the AWS + Terraform path is the most complete first deployment path today. + --- ## Why This Framework Is Needed diff --git a/bin/cli.js b/bin/cli.js index 599d044..376a342 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -196,6 +196,7 @@ program .option('--rule-group-only', 'AWS only: generate WAF rule groups without aws_wafv2_web_acl output') .option('--fail-on-permissive', 'Exit non-zero when policy.metadata.risk_level is "permissive" (gate for production CI)') .option('--fail-on-waf-approximation', 'Cloudflare only: exit non-zero when the policy relies on approximate or unsupported Cloudflare WAF mappings (see docs/cloudflare-waf-parity.md)') + .option('--allow-placeholder-token', 'Allow non-production placeholder credentials for static_token/basic_auth gates when referenced env vars are unset') .action((opts) => { const { compile } = require(path.join(pkgRoot, 'lib')); const cwd = process.cwd(); @@ -213,6 +214,7 @@ program ruleGroupOnly: !!opts.ruleGroupOnly, failOnPermissive: !!opts.failOnPermissive, failOnWafApproximation: !!opts.failOnWafApproximation, + allowPlaceholderToken: !!opts.allowPlaceholderToken, cwd, pkgRoot, }); diff --git a/docs/iac.ja.md b/docs/iac.ja.md index 1ae364d..bafea3d 100644 --- a/docs/iac.ja.md +++ b/docs/iac.ja.md @@ -6,14 +6,14 @@ ## 概要 -`npx cdn-security build`(および Infra 用に `npx cdn-security build --target aws`)のあと: +`npx cdn-security build` のあと: | 出力 | 用途 | |------|------| | **dist/edge/viewer-request.js** | CloudFront Function (Viewer Request) | | **dist/edge/viewer-response.js** | CloudFront Function (Viewer Response) | | **dist/edge/cloudflare/index.ts** | Cloudflare Worker(`--target cloudflare` でビルドした場合)。出力は TypeScript。Wrangler がデプロイ時にコンパイルする。Wrangler を使わない場合は TypeScript ビルド環境が必要。 | -| **dist/infra/waf-rules.tf.json** | Terraform JSON: `aws_wafv2_rule_group`(レートベースルール)。ポリシーに `firewall.waf` がある場合に生成。 | +| **dist/infra/waf-rules.tf.json** | Terraform JSON: WAFv2 rule group / Web ACL resources。ポリシーに `firewall.waf` がある場合に生成。 | | **dist/infra/waf-cloudformation.json** | AWS CloudFormation: `AWS::WAFv2::*` リソース。`emit-waf --format cloudformation` で生成。 | --- @@ -72,43 +72,38 @@ resource "aws_cloudfront_distribution" "main" { ## Terraform: WAF(Infra) -ポリシーに **`firewall.waf`** セクションがあると、ビルドで **`dist/infra/waf-rules.tf.json`**(Terraform JSON)が出力されます。利用方法: +ポリシーに **`firewall.waf`** セクションがあると、ビルドで **`dist/infra/waf-rules.tf.json`**(Terraform JSON)が出力されます。最も単純な production path は、このファイルを CloudFront distribution を管理している Terraform root にコピーまたは生成し、生成された Web ACL ARN を distribution から参照する形です。 -- **方法 A**: この JSON ファイルを Terraform のディレクトリに含め、`terraform plan` / `apply` でルールグループを管理する。 -- **方法 B**: 生成されたルールグループを Web ACL から参照する。 +### 推奨レイアウト -### 生成 waf-rules.tf.json の利用 +```text +infra/ + main.tf + cdn-security.auto.tf.json # dist/infra/waf-rules.tf.json からコピー +``` -ファイルはそのまま Terraform JSON として有効です。 +ビルドしてコピー: -1. **Terraform モジュールにコピー**: `waf-rules.tf.json` を Terraform のディレクトリに置き、そのディレクトリで `terraform plan` / `apply` を実行する。 -2. **別モジュールから参照**: このルールグループを Web ACL の `rule_group_reference_statement` で参照する。 +```bash +EDGE_ADMIN_TOKEN=replace-with-a-deploy-secret npx cdn-security build +cp dist/infra/waf-rules.tf.json infra/cdn-security.auto.tf.json +``` -例: ビルド実行後、Terraform の設定ディレクトリで: +その後、生成された Web ACL を CloudFront distribution にアタッチします。生成リソース名には policy の `project` を sanitize した値が入ります。`project: example-cdn-security` の場合、Web ACL resource name は `aws_wafv2_web_acl.example_cdn_security` です。 ```hcl -# 同一リポジトリ、または dist/infra/waf-rules.tf.json をこのディレクトリにコピー -# Web ACL でルールグループを参照: -resource "aws_wafv2_web_acl" "main" { - name = "main" - scope = "REGIONAL" - default_action { allow {} } - rule { - name = "rate-limit" - priority = 1 - override_action { none {} } - statement { - rule_group_reference_statement { - arn = aws_wafv2_rule_group.example_cdn_security_rate_limit[0].arn - } - } - visibility_config { ... } +resource "aws_cloudfront_distribution" "main" { + # ... + + web_acl_id = aws_wafv2_web_acl.example_cdn_security.arn + + default_cache_behavior { + # ... } - visibility_config { ... } } ``` -`dist/infra/` をそのまま使う場合は、リポジトリルートや `dist/infra/` を含むディレクトリで Terraform を実行すると、ルールグループが同じ state で定義されます。 +既存 Web ACL を別で管理している場合は、`npx cdn-security build --rule-group-only` を実行し、手書きの `aws_wafv2_web_acl` から生成 rule group を参照してください。生成 JSON は Web ACL と同じ Terraform state に置く必要があります。そうでないと Terraform から直接参照できません。 --- diff --git a/docs/iac.md b/docs/iac.md index 635a294..ffb9151 100644 --- a/docs/iac.md +++ b/docs/iac.md @@ -6,14 +6,14 @@ This document describes how to use the generated **`dist/edge/`** and **`dist/in ## Overview -After `npx cdn-security build` (and `npx cdn-security build --target aws` for Infra): +After `npx cdn-security build`: | Output | Use | |--------|-----| | **dist/edge/viewer-request.js** | CloudFront Function (Viewer Request) | | **dist/edge/viewer-response.js** | CloudFront Function (Viewer Response) | | **dist/edge/cloudflare/index.ts** | Cloudflare Worker (when built with `--target cloudflare`). Output is TypeScript; Wrangler compiles it on deploy. Without Wrangler, a TypeScript build step is required. | -| **dist/infra/waf-rules.tf.json** | Terraform JSON: `aws_wafv2_rule_group` (rate-based rule). Use when policy has `firewall.waf`. | +| **dist/infra/waf-rules.tf.json** | Terraform JSON: WAFv2 rule group / Web ACL resources. Use when policy has `firewall.waf`. | | **dist/infra/waf-cloudformation.json** | AWS CloudFormation: `AWS::WAFv2::*` resources. Generate with `emit-waf --format cloudformation`. | --- @@ -72,43 +72,38 @@ If you generate `dist/edge/origin-request.js`, zip it and use `aws_lambda_functi ## Terraform: WAF (Infra) -When your policy includes a **`firewall.waf`** section, the build outputs **`dist/infra/waf-rules.tf.json`** (Terraform JSON). You can: +When your policy includes a **`firewall.waf`** section, the build outputs **`dist/infra/waf-rules.tf.json`** (Terraform JSON). The simplest production path is to copy or generate that file into the same Terraform root that owns your CloudFront distribution, then reference the generated Web ACL ARN from your distribution. -- **Option A**: Import the rule group by referencing the JSON file (e.g. `terraform plan` in a directory that includes this file, or use `terraform import` if the resource is created elsewhere). -- **Option B**: Use the generated rule group in your Terraform by **inlining** or **reading** the JSON and creating an `aws_wafv2_rule_group` resource that matches. +### Recommended layout -### Using the generated waf-rules.tf.json +```text +infra/ + main.tf + cdn-security.auto.tf.json # copied from dist/infra/waf-rules.tf.json +``` -The file is valid Terraform JSON. You can: +Build and copy: -1. **Copy into your Terraform module**: Place `waf-rules.tf.json` in a Terraform directory and run `terraform plan` / `apply` in that directory; Terraform will manage the rule group. -2. **Reference from another module**: Use a module that reads the JSON or use `terraform import` to attach the rule group to your Web ACL. +```bash +EDGE_ADMIN_TOKEN=replace-with-a-deploy-secret npx cdn-security build +cp dist/infra/waf-rules.tf.json infra/cdn-security.auto.tf.json +``` -Example: ensure the build has run, then in a Terraform config directory: +Then attach the generated Web ACL. The generated resource names include the sanitized `project` value from your policy; if your policy uses `project: example-cdn-security`, the Web ACL resource name is `aws_wafv2_web_acl.example_cdn_security`. ```hcl -# In the same repo, or copy dist/infra/waf-rules.tf.json into this directory -# Then reference the rule group in your Web ACL: -resource "aws_wafv2_web_acl" "main" { - name = "main" - scope = "REGIONAL" - default_action { allow {} } - rule { - name = "rate-limit" - priority = 1 - override_action { none {} } - statement { - rule_group_reference_statement { - arn = aws_wafv2_rule_group.example_cdn_security_rate_limit[0].arn - } - } - visibility_config { ... } +resource "aws_cloudfront_distribution" "main" { + # ... + + web_acl_id = aws_wafv2_web_acl.example_cdn_security.arn + + default_cache_behavior { + # ... } - visibility_config { ... } } ``` -If you keep the generated file in `dist/infra/`, run Terraform from the repo root or from a subdirectory that includes `dist/infra/waf-rules.tf.json` so the rule group is defined in the same state. +For existing Web ACL ownership, run `npx cdn-security build --rule-group-only` and attach the generated rule group from your hand-written `aws_wafv2_web_acl`. Keep the generated JSON in the same Terraform state as the Web ACL, otherwise Terraform cannot reference the generated resources directly. --- diff --git a/docs/quickstart.ja.md b/docs/quickstart.ja.md index 09919a3..2c9985e 100644 --- a/docs/quickstart.ja.md +++ b/docs/quickstart.ja.md @@ -11,15 +11,20 @@ npm install --save-dev cdn-security-framework npx cdn-security init ``` -プラットフォーム(AWS CloudFront / Cloudflare Workers)とプロファイル(Strict / Balanced / Permissive)を選ぶと、`policy/security.yml` と `policy/profiles/.yml` が作成されます。 +プラットフォーム(AWS CloudFront / Cloudflare Workers)と、プロファイル(Strict / Balanced / Permissive)またはアーキタイプ(SPA / REST API / 管理画面 / マイクロサービス)を選ぶと、`policy/security.yml` と `policy/profiles/` または `policy/archetypes/` 配下の参照コピーが作成されます。 非対話: `npx cdn-security init --platform aws --profile balanced --force` +最初の推奨ルート: `npx cdn-security init --platform aws --archetype spa-static-site --force` ## 2. ポリシー編集とビルド `policy/security.yml` を必要に応じて編集し(allow_methods、block ルール、routes など)、次を実行します。 ```bash +# policy に static_token 認証ゲートがある場合は、参照先の build-time secret を +# 先に設定します。組み込みの base/admin 例は EDGE_ADMIN_TOKEN を使います。 +export EDGE_ADMIN_TOKEN=replace-with-a-deploy-secret + npx cdn-security build # Cloudflare Workers @@ -33,13 +38,16 @@ npx cdn-security build --target cloudflare `/admin`、`/docs`、`/swagger` の保護用: - 環境変数や CDN のシークレット管理(Terraform、Wrangler など)で `EDGE_ADMIN_TOKEN` を設定してください。 -- ビルド時に変数が設定されていれば注入され、未設定の場合はプレースホルダーが入り、デプロイパイプラインで差し替え可能です。 +- ビルド時に変数が設定されていれば注入されます。local fixture build のみ、`npx cdn-security build --allow-placeholder-token` で明示的な insecure placeholder と警告を出せます。placeholder artifact は絶対にデプロイしないでください。 `viewer-request.js` を手で編集する必要はありません。トークンはポリシー(routes.auth_gate.token_env)と環境変数で制御されます。 ## 4. テスト ```bash +export EDGE_ADMIN_TOKEN=ci-build-token-not-for-deploy +export ORIGIN_SECRET=ci-origin-secret-not-for-deploy + npm run test:runtime npm run test:unit npm run test:drift diff --git a/docs/quickstart.md b/docs/quickstart.md index c0a9a42..cb39c34 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -11,15 +11,20 @@ npm install --save-dev cdn-security-framework npx cdn-security init ``` -Choose platform (AWS CloudFront / Cloudflare Workers) and profile (Strict / Balanced / Permissive). This creates `policy/security.yml` and `policy/profiles/.yml`. +Choose platform (AWS CloudFront / Cloudflare Workers) and either a profile (Strict / Balanced / Permissive) or an archetype (SPA, REST API, admin, microservice). This creates `policy/security.yml` and a reference copy under `policy/profiles/` or `policy/archetypes/`. Non-interactive: `npx cdn-security init --platform aws --profile balanced --force` +Recommended first path: `npx cdn-security init --platform aws --archetype spa-static-site --force` ## 2. Edit policy and build Edit `policy/security.yml` as needed (allow_methods, block rules, routes, etc.), then: ```bash +# If your policy has a static_token auth gate, set the referenced build-time +# secret first. The built-in base/admin examples use EDGE_ADMIN_TOKEN. +export EDGE_ADMIN_TOKEN=replace-with-a-deploy-secret + # AWS (default) npx cdn-security build @@ -34,13 +39,16 @@ This validates the policy and generates **Edge Runtime** code into `dist/edge/`. For `/admin`, `/docs`, `/swagger` protection: - Set `EDGE_ADMIN_TOKEN` in your environment or CDN secret management (e.g. Terraform, Wrangler). -- The build injects it at compile time when the variable is set; otherwise it uses a placeholder you can replace in your deployment pipeline. +- The build injects it at compile time when the variable is set. For local fixture builds only, `npx cdn-security build --allow-placeholder-token` emits an explicit insecure placeholder and a warning. Never deploy placeholder artifacts. You do **not** edit `viewer-request.js` by hand; the token is driven by policy (routes.auth_gate.token_env) and environment. ## 4. Test ```bash +export EDGE_ADMIN_TOKEN=ci-build-token-not-for-deploy +export ORIGIN_SECRET=ci-origin-secret-not-for-deploy + npm run test:runtime npm run test:unit npm run test:drift diff --git a/emitter/index.d.ts b/emitter/index.d.ts index 0cdf717..b96e196 100644 --- a/emitter/index.d.ts +++ b/emitter/index.d.ts @@ -5,6 +5,7 @@ export type CompileArtifactsOptions = { target?: CompileTarget; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; outputMode?: string; ruleGroupOnly?: boolean; cwd?: string; diff --git a/emitter/index.js b/emitter/index.js index 2e54a04..142de42 100644 --- a/emitter/index.js +++ b/emitter/index.js @@ -84,6 +84,7 @@ function compileArtifacts(opts = {}) { return { ok: false, errors, warnings, ...baseResult }; } const permissiveFlag = opts.failOnPermissive ? ['--fail-on-permissive'] : []; + const placeholderFlag = opts.allowPlaceholderToken ? ['--allow-placeholder-token'] : []; if (target === 'aws') { const compilePath = path.join(pkgRoot, 'scripts', 'compile.js'); const compileResult = spawnSync(process.execPath, [ @@ -91,6 +92,7 @@ function compileArtifacts(opts = {}) { '--policy', policyPath, '--out-dir', outDir, ...permissiveFlag, + ...placeholderFlag, ], { cwd, encoding: 'utf8', env }); if (compileResult.status !== 0) { collectFailedSpawn('edge compile', compileResult, errors); @@ -120,6 +122,7 @@ function compileArtifacts(opts = {}) { '--policy', policyPath, '--out-dir', outDir, ...permissiveFlag, + ...placeholderFlag, ], { cwd, encoding: 'utf8', env }); if (cfResult.status !== 0) { collectFailedSpawn('cloudflare edge compile', cfResult, errors); diff --git a/lib/index.d.ts b/lib/index.d.ts index 06d28fb..8b76792 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -35,6 +35,7 @@ export interface CompileOptions { target?: CompileTarget; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; outputMode?: 'full' | 'rule-group' | string; ruleGroupOnly?: boolean; cwd?: string; diff --git a/package.json b/package.json index db6ccdf..4927426 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,18 @@ "scripts/policy-lint.d.ts", "scripts/lib/", "templates/", + "examples/README.md", + "examples/README.ja.md", + "examples/aws-cloudfront/README.md", + "examples/aws-cloudfront/README.ja.md", + "examples/aws-cloudfront/package.json", + "examples/aws-cloudfront/package-lock.json", + "examples/aws-cloudfront/policy/", + "examples/cloudflare/README.md", + "examples/cloudflare/README.ja.md", + "examples/cloudflare/package.json", + "examples/cloudflare/package-lock.json", + "examples/cloudflare/policy/", "policy/base.yml", "policy/profiles/", "policy/archetypes/", diff --git a/scripts/programmatic-api-unit-tests.js b/scripts/programmatic-api-unit-tests.js index edbbbab..7b4b187 100644 --- a/scripts/programmatic-api-unit-tests.js +++ b/scripts/programmatic-api-unit-tests.js @@ -47,6 +47,28 @@ firewall: managed_rules: - AWSManagedRulesCommonRuleSet `; +const STATIC_TOKEN_POLICY = ` +version: 1 +project: token-test +request: + allow_methods: [GET, POST] +routes: + - name: admin + match: + path_prefixes: ["/admin"] + auth_gate: + type: static_token + header: x-edge-token + token_env: EDGE_ADMIN_TOKEN +response_headers: + hsts: "max-age=1" +firewall: + waf: + scope: CLOUDFRONT + rate_limit: 1000 + managed_rules: + - AWSManagedRulesCommonRuleSet +`; const BROKEN_POLICY = ` version: 1 request: @@ -171,6 +193,28 @@ test('compile: aws target writes edge + infra, returns file lists', () => { ctx.cleanup(); } }); +test('compile: allowPlaceholderToken permits non-production build without auth env', () => { + const ctx = tmpProject(STATIC_TOKEN_POLICY); + try { + const result = api.compile({ + policyPath: ctx.policyPath, + outDir: ctx.outDir, + target: 'aws', + allowPlaceholderToken: true, + env: Object.assign({}, process.env, { + EDGE_ADMIN_TOKEN: '', + ORIGIN_SECRET: 'ci-origin-secret-not-for-deploy', + }), + }); + assert.strictEqual(result.ok, true, `compile failed: ${result.errors.join(' ')}`); + const viewer = fs.readFileSync(path.join(ctx.outDir, 'edge', 'viewer-request.js'), 'utf8'); + assert.ok(viewer.includes('INSECURE_PLACEHOLDER__REBUILD_WITH_REAL_TOKEN')); + assert.ok(result.warnings.some((w) => /allow-placeholder-token/.test(w))); + } + finally { + ctx.cleanup(); + } +}); test('compile: unknown target returns structured error', () => { const result = api.compile({ policyPath: '/tmp/nothing.yml', @@ -350,6 +394,33 @@ test('CLI backwards-compat: `cdn-security build --target aws` still succeeds', ( ctx.cleanup(); } }); +test('CLI authoring DX: build --allow-placeholder-token succeeds without auth env', () => { + const ctx = tmpProject(BASIC_AWS_POLICY); + try { + const { spawnSync } = require('child_process'); + const cli = path.join(repoRoot, 'bin', 'cli.js'); + const result = spawnSync(process.execPath, [ + cli, 'build', + '-p', ctx.policyPath, + '-o', ctx.outDir, + '--target', 'aws', + '--allow-placeholder-token', + ], { + cwd: ctx.tmp, + encoding: 'utf8', + env: Object.assign({}, process.env, { + EDGE_ADMIN_TOKEN: '', + ORIGIN_SECRET: 'ci-origin-secret-not-for-deploy', + }), + }); + assert.strictEqual(result.status, 0, `CLI build failed: ${result.stderr}`); + assert.ok(result.stderr.includes('Generated artifacts are NOT safe for production')); + assert.ok(fs.existsSync(path.join(ctx.outDir, 'edge', 'viewer-request.js'))); + } + finally { + ctx.cleanup(); + } +}); test('CLI authoring DX: explain summarizes policy posture', () => { const ctx = tmpProject(BASIC_AWS_POLICY); try { diff --git a/src/bin/cli.ts b/src/bin/cli.ts index d4671e3..351f956 100755 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -27,6 +27,7 @@ type BuildOptions = { ruleGroupOnly?: boolean; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; }; type DoctorOptions = { @@ -264,6 +265,7 @@ program .option('--rule-group-only', 'AWS only: generate WAF rule groups without aws_wafv2_web_acl output') .option('--fail-on-permissive', 'Exit non-zero when policy.metadata.risk_level is "permissive" (gate for production CI)') .option('--fail-on-waf-approximation', 'Cloudflare only: exit non-zero when the policy relies on approximate or unsupported Cloudflare WAF mappings (see docs/cloudflare-waf-parity.md)') + .option('--allow-placeholder-token', 'Allow non-production placeholder credentials for static_token/basic_auth gates when referenced env vars are unset') .action((opts: BuildOptions) => { const { compile } = require(path.join(pkgRoot, 'lib')); const cwd = process.cwd(); @@ -282,6 +284,7 @@ program ruleGroupOnly: !!opts.ruleGroupOnly, failOnPermissive: !!opts.failOnPermissive, failOnWafApproximation: !!opts.failOnWafApproximation, + allowPlaceholderToken: !!opts.allowPlaceholderToken, cwd, pkgRoot, }); diff --git a/src/emitter/index.ts b/src/emitter/index.ts index bddb74a..7505a9e 100644 --- a/src/emitter/index.ts +++ b/src/emitter/index.ts @@ -19,6 +19,7 @@ export type CompileArtifactsOptions = { target?: CompileTarget; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; outputMode?: string; ruleGroupOnly?: boolean; cwd?: string; @@ -126,6 +127,7 @@ export function compileArtifacts(opts: CompileArtifactsOptions = {}): CompileArt } const permissiveFlag = opts.failOnPermissive ? ['--fail-on-permissive'] : []; + const placeholderFlag = opts.allowPlaceholderToken ? ['--allow-placeholder-token'] : []; if (target === 'aws') { const compilePath = path.join(pkgRoot, 'scripts', 'compile.js'); @@ -136,6 +138,7 @@ export function compileArtifacts(opts: CompileArtifactsOptions = {}): CompileArt '--policy', policyPath, '--out-dir', outDir, ...permissiveFlag, + ...placeholderFlag, ], { cwd, encoding: 'utf8', env }, ); @@ -173,6 +176,7 @@ export function compileArtifacts(opts: CompileArtifactsOptions = {}): CompileArt '--policy', policyPath, '--out-dir', outDir, ...permissiveFlag, + ...placeholderFlag, ], { cwd, encoding: 'utf8', env }, ); diff --git a/src/lib/compile.ts b/src/lib/compile.ts index 6fb5f4d..d045859 100644 --- a/src/lib/compile.ts +++ b/src/lib/compile.ts @@ -16,6 +16,7 @@ interface CompileOptions { target?: CompileTarget; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; outputMode?: string; ruleGroupOnly?: boolean; cwd?: string; diff --git a/src/lib/index.ts b/src/lib/index.ts index 88d1d06..ae48d6e 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -38,6 +38,7 @@ export interface CompileOptions { target?: CompileTarget; failOnPermissive?: boolean; failOnWafApproximation?: boolean; + allowPlaceholderToken?: boolean; outputMode?: 'full' | 'rule-group' | string; ruleGroupOnly?: boolean; cwd?: string; diff --git a/src/scripts/programmatic-api-unit-tests.ts b/src/scripts/programmatic-api-unit-tests.ts index 0eec739..9e6a9c5 100644 --- a/src/scripts/programmatic-api-unit-tests.ts +++ b/src/scripts/programmatic-api-unit-tests.ts @@ -50,6 +50,29 @@ firewall: - AWSManagedRulesCommonRuleSet `; +const STATIC_TOKEN_POLICY = ` +version: 1 +project: token-test +request: + allow_methods: [GET, POST] +routes: + - name: admin + match: + path_prefixes: ["/admin"] + auth_gate: + type: static_token + header: x-edge-token + token_env: EDGE_ADMIN_TOKEN +response_headers: + hsts: "max-age=1" +firewall: + waf: + scope: CLOUDFRONT + rate_limit: 1000 + managed_rules: + - AWSManagedRulesCommonRuleSet +`; + const BROKEN_POLICY = ` version: 1 request: @@ -184,6 +207,28 @@ test('compile: aws target writes edge + infra, returns file lists', () => { } }); +test('compile: allowPlaceholderToken permits non-production build without auth env', () => { + const ctx = tmpProject(STATIC_TOKEN_POLICY); + try { + const result = api.compile({ + policyPath: ctx.policyPath, + outDir: ctx.outDir, + target: 'aws', + allowPlaceholderToken: true, + env: Object.assign({}, process.env, { + EDGE_ADMIN_TOKEN: '', + ORIGIN_SECRET: 'ci-origin-secret-not-for-deploy', + }), + }); + assert.strictEqual(result.ok, true, `compile failed: ${result.errors.join(' ')}`); + const viewer = fs.readFileSync(path.join(ctx.outDir, 'edge', 'viewer-request.js'), 'utf8'); + assert.ok(viewer.includes('INSECURE_PLACEHOLDER__REBUILD_WITH_REAL_TOKEN')); + assert.ok(result.warnings.some((w: string) => /allow-placeholder-token/.test(w))); + } finally { + ctx.cleanup(); + } +}); + test('compile: unknown target returns structured error', () => { const result = api.compile({ policyPath: '/tmp/nothing.yml', @@ -372,6 +417,33 @@ test('CLI backwards-compat: `cdn-security build --target aws` still succeeds', ( } }); +test('CLI authoring DX: build --allow-placeholder-token succeeds without auth env', () => { + const ctx = tmpProject(BASIC_AWS_POLICY); + try { + const { spawnSync } = require('child_process'); + const cli = path.join(repoRoot, 'bin', 'cli.js'); + const result: any = spawnSync(process.execPath, [ + cli, 'build', + '-p', ctx.policyPath, + '-o', ctx.outDir, + '--target', 'aws', + '--allow-placeholder-token', + ], { + cwd: ctx.tmp, + encoding: 'utf8', + env: Object.assign({}, process.env, { + EDGE_ADMIN_TOKEN: '', + ORIGIN_SECRET: 'ci-origin-secret-not-for-deploy', + }), + }); + assert.strictEqual(result.status, 0, `CLI build failed: ${result.stderr}`); + assert.ok(result.stderr.includes('Generated artifacts are NOT safe for production')); + assert.ok(fs.existsSync(path.join(ctx.outDir, 'edge', 'viewer-request.js'))); + } finally { + ctx.cleanup(); + } +}); + test('CLI authoring DX: explain summarizes policy posture', () => { const ctx = tmpProject(BASIC_AWS_POLICY); try {