@intentius/chant 0.49.0 → 0.51.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/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/op-progress.d.ts +57 -0
- package/dist/cli/handlers/op-progress.d.ts.map +1 -0
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/run-client.d.ts +21 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run-report.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/handlers/search.d.ts +22 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +47 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +7 -5
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +134 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts +77 -0
- package/dist/lifecycle/assert-live.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +17 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/disruption.d.ts +96 -0
- package/dist/lifecycle/disruption.d.ts.map +1 -0
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/replay.d.ts +2 -0
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +7 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +23 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.test.ts +90 -0
- package/src/cli/handlers/lifecycle.ts +30 -5
- package/src/cli/handlers/op-progress.test.ts +202 -0
- package/src/cli/handlers/op-progress.ts +192 -0
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/run-client.test.ts +82 -0
- package/src/cli/handlers/run-client.ts +85 -2
- package/src/cli/handlers/run-report.test.ts +62 -0
- package/src/cli/handlers/run-report.ts +20 -58
- package/src/cli/handlers/run.test.ts +144 -0
- package/src/cli/handlers/run.ts +40 -18
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/handlers/search-drift.test.ts +263 -0
- package/src/cli/handlers/search.ts +150 -1
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +81 -1
- package/src/cli/mcp/op-tools.ts +17 -6
- package/src/cli/mcp/resource-handlers.ts +13 -5
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +47 -3
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/run-progress.ts +9 -5
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +154 -2
- package/src/lifecycle/assert-live.test.ts +125 -0
- package/src/lifecycle/assert-live.ts +154 -0
- package/src/lifecycle/change-set.ts +35 -3
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/disruption.test.ts +186 -0
- package/src/lifecycle/disruption.ts +224 -0
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/replay.test.ts +25 -0
- package/src/lifecycle/replay.ts +11 -3
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +52 -10
- package/src/op/local-output.ts +1 -1
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/testing.test.ts +89 -2
- package/src/testing.ts +63 -3
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
+
import Ajv from "ajv";
|
|
3
4
|
import { auditCommand, tokenForHost, coverageNotes, installLine, NO_LEXICONS_EXIT_CODE } from "./audit";
|
|
4
5
|
import { discoverByDetection, loadAuditPlugins } from "../../audit/discover";
|
|
5
6
|
import { MissingLexiconError, type AuditInput, type AuditLexicon } from "../../audit/core";
|
|
@@ -8,12 +9,19 @@ import { MissingLexiconError, type AuditInput, type AuditLexicon } from "../../a
|
|
|
8
9
|
async function discoverLexicon(repo: string, lexicon: AuditLexicon): Promise<AuditInput[]> {
|
|
9
10
|
return discoverByDetection(repo, await loadAuditPlugins()).filter((i) => i.lexicon === lexicon);
|
|
10
11
|
}
|
|
11
|
-
import { readFileSync, existsSync, rmSync } from "fs";
|
|
12
|
+
import { readFileSync, existsSync, rmSync, mkdirSync, writeFileSync } from "fs";
|
|
12
13
|
import { tmpdir } from "os";
|
|
13
14
|
import { join } from "path";
|
|
15
|
+
import { fingerprintSecret } from "../../audit/secrets";
|
|
14
16
|
|
|
15
17
|
const REPO = fileURLToPath(new URL("./__fixtures__/audit-repo", import.meta.url));
|
|
16
18
|
|
|
19
|
+
/** The real, vendored SARIF 2.1.0 JSON Schema — same structural-validation convention as component-bom.test.ts. */
|
|
20
|
+
function loadSarifSchema(): unknown {
|
|
21
|
+
const path = fileURLToPath(new URL("./__fixtures__/schemas/sarif-2.1.0.schema.json", import.meta.url));
|
|
22
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
describe("auditCommand", () => {
|
|
18
26
|
test("selects a host-specific token (no cross-host leakage)", () => {
|
|
19
27
|
const env = { GITHUB_TOKEN: "gh", GITLAB_TOKEN: "gl", CODEBERG_TOKEN: "cb" } as unknown as NodeJS.ProcessEnv;
|
|
@@ -399,4 +407,210 @@ describe("auditCommand", () => {
|
|
|
399
407
|
expect(sarif.version).toBe("2.1.0");
|
|
400
408
|
expect(sarif.runs[0].results.length).toBeGreaterThan(0);
|
|
401
409
|
});
|
|
410
|
+
|
|
411
|
+
test("sarif carries tier on results and dimension/category + remediation help on rules (#442)", async () => {
|
|
412
|
+
const result = await auditCommand({ path: REPO, format: "sarif" });
|
|
413
|
+
const sarif = JSON.parse(result.output) as {
|
|
414
|
+
runs: Array<{
|
|
415
|
+
tool: { driver: { rules: Array<{ id: string; helpUri?: string; help?: { text: string }; properties?: { category?: string; dimension?: string } }> } };
|
|
416
|
+
results: Array<{ ruleId: string; properties?: { tier?: string } }>;
|
|
417
|
+
}>;
|
|
418
|
+
};
|
|
419
|
+
const run = sarif.runs[0];
|
|
420
|
+
// Every result carries the finding's tier so a consumer can triage merge-worthy vs. report-only without a second lookup.
|
|
421
|
+
expect(run.results.length).toBeGreaterThan(0);
|
|
422
|
+
for (const r of run.results) {
|
|
423
|
+
expect(r.properties?.tier).toMatch(/^(merge-worthy|report-only)$/);
|
|
424
|
+
}
|
|
425
|
+
// Every rule carries its dimension/category and remediation text (mapped from the catalog, #351) as help.
|
|
426
|
+
expect(run.tool.driver.rules.length).toBeGreaterThan(0);
|
|
427
|
+
for (const rule of run.tool.driver.rules) {
|
|
428
|
+
expect(rule.properties?.category).toMatch(/^(security|correctness|best-practice)$/);
|
|
429
|
+
expect(rule.properties?.dimension).toBe(rule.properties?.category);
|
|
430
|
+
expect(rule.help?.text.length).toBeGreaterThan(0);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("sarif output validates against the real, vendored SARIF 2.1.0 JSON Schema", async () => {
|
|
435
|
+
const result = await auditCommand({ path: REPO, format: "sarif" });
|
|
436
|
+
const doc = JSON.parse(result.output);
|
|
437
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
438
|
+
const validate = ajv.compile(loadSarifSchema() as object);
|
|
439
|
+
const valid = validate(doc);
|
|
440
|
+
if (!valid) throw new Error(ajv.errorsText(validate.errors));
|
|
441
|
+
expect(valid).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("sarif schema validation also holds for the empty-result case (no findings)", async () => {
|
|
445
|
+
const repo = fileURLToPath(new URL("./__fixtures__/audit-fountain-clean", import.meta.url));
|
|
446
|
+
const result = await auditCommand({ path: repo, format: "sarif" });
|
|
447
|
+
const doc = JSON.parse(result.output);
|
|
448
|
+
expect(doc.runs[0].results).toEqual([]);
|
|
449
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
450
|
+
const validate = ajv.compile(loadSarifSchema() as object);
|
|
451
|
+
const valid = validate(doc);
|
|
452
|
+
if (!valid) throw new Error(ajv.errorsText(validate.errors));
|
|
453
|
+
expect(valid).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe("secrets detection (#443)", () => {
|
|
458
|
+
// A deliberately fake, non-functional AWS-access-key-ID-shaped value, built
|
|
459
|
+
// via concatenation (never a contiguous literal in this file's raw bytes)
|
|
460
|
+
// so pushing this file doesn't trip GitHub's own secret-scanning push
|
|
461
|
+
// protection on what is just a test fixture.
|
|
462
|
+
const FAKE_AWS_KEY = "AKIA" + "ABCDEFGHIJKLMNOP";
|
|
463
|
+
const FAKE_STRIPE_LIVE = "sk_live_" + "4eC39HqLyjWDarjtT1zdp7dc";
|
|
464
|
+
|
|
465
|
+
function tmpRepo(): string {
|
|
466
|
+
const dir = join(tmpdir(), `chant-audit-secrets-${process.pid}-${Math.random().toString(36).slice(2)}`);
|
|
467
|
+
mkdirSync(dir, { recursive: true });
|
|
468
|
+
return dir;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
test("flags a hardcoded credential even when no lexicon is installed", async () => {
|
|
472
|
+
const dir = tmpRepo();
|
|
473
|
+
writeFileSync(join(dir, ".env"), `AWS_ACCESS_KEY_ID=${FAKE_AWS_KEY}\n`);
|
|
474
|
+
const result = await auditCommand({ path: dir, plugins: [] });
|
|
475
|
+
expect(result.status).toBe("no-lexicons"); // no lexicon installed — still not a clean miss
|
|
476
|
+
expect(result.findings.map((f) => f.checkId)).toContain("SEC001");
|
|
477
|
+
expect(JSON.stringify(result.findings)).not.toContain(FAKE_AWS_KEY); // redaction
|
|
478
|
+
rmSync(dir, { recursive: true, force: true });
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("sarif output never carries the raw secret value for a planted fake credential (#442)", async () => {
|
|
482
|
+
// `plugins: []` takes the "no-lexicons" diagnostic branch regardless of
|
|
483
|
+
// format, so a real lexicon (github) has to be installed for `format:
|
|
484
|
+
// "sarif"` to actually render SARIF here — same setup as the JSON
|
|
485
|
+
// round-trip test below.
|
|
486
|
+
const dir = tmpRepo();
|
|
487
|
+
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
|
|
488
|
+
writeFileSync(
|
|
489
|
+
join(dir, ".github", "workflows", "ci.yml"),
|
|
490
|
+
"name: CI\non:\n push:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n",
|
|
491
|
+
);
|
|
492
|
+
writeFileSync(join(dir, ".env"), `AWS_ACCESS_KEY_ID=${FAKE_AWS_KEY}\nSTRIPE_KEY=${FAKE_STRIPE_LIVE}\n`);
|
|
493
|
+
const all = await loadAuditPlugins();
|
|
494
|
+
const result = await auditCommand({ path: dir, plugins: all.filter((p) => p.name === "github"), format: "sarif" });
|
|
495
|
+
expect(result.status).toBe("ok");
|
|
496
|
+
expect(result.output).not.toContain(FAKE_AWS_KEY);
|
|
497
|
+
expect(result.output).not.toContain(FAKE_STRIPE_LIVE);
|
|
498
|
+
const sarif = JSON.parse(result.output) as { runs: Array<{ results: Array<{ ruleId: string; message: { text: string } }> }> };
|
|
499
|
+
const ruleIds = sarif.runs[0].results.map((r) => r.ruleId);
|
|
500
|
+
expect(ruleIds).toContain("SEC001");
|
|
501
|
+
expect(ruleIds).toContain("SEC006");
|
|
502
|
+
for (const r of sarif.runs[0].results) {
|
|
503
|
+
expect(r.message.text).not.toContain(FAKE_AWS_KEY);
|
|
504
|
+
expect(r.message.text).not.toContain(FAKE_STRIPE_LIVE);
|
|
505
|
+
}
|
|
506
|
+
rmSync(dir, { recursive: true, force: true });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("a secret rides alongside lexicon findings, tiered and cataloged like any other finding", async () => {
|
|
510
|
+
const dir = tmpRepo();
|
|
511
|
+
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
|
|
512
|
+
writeFileSync(
|
|
513
|
+
join(dir, ".github", "workflows", "ci.yml"),
|
|
514
|
+
"name: CI\non:\n push:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n",
|
|
515
|
+
);
|
|
516
|
+
writeFileSync(join(dir, ".env"), `STRIPE_KEY=${FAKE_STRIPE_LIVE}\n`);
|
|
517
|
+
const all = await loadAuditPlugins();
|
|
518
|
+
const result = await auditCommand({ path: dir, plugins: all.filter((p) => p.name === "github"), format: "json" });
|
|
519
|
+
expect(result.status).toBe("ok");
|
|
520
|
+
const json = JSON.parse(result.output);
|
|
521
|
+
const sec = json.findings.find((f: { checkId: string }) => f.checkId === "SEC006");
|
|
522
|
+
expect(sec).toBeDefined();
|
|
523
|
+
expect(sec.tier).toBe("merge-worthy");
|
|
524
|
+
expect(sec.category).toBe("security");
|
|
525
|
+
expect(sec.file).toBe(".env");
|
|
526
|
+
expect(JSON.stringify(json)).not.toContain(FAKE_STRIPE_LIVE); // redaction, incl. the JSON report
|
|
527
|
+
rmSync(dir, { recursive: true, force: true });
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("an inline `chant-audit-ignore` marker suppresses the finding", async () => {
|
|
531
|
+
const dir = tmpRepo();
|
|
532
|
+
writeFileSync(join(dir, ".env"), `AWS_ACCESS_KEY_ID=${FAKE_AWS_KEY} # chant-audit-ignore: SEC001\n`);
|
|
533
|
+
const result = await auditCommand({ path: dir, plugins: [] });
|
|
534
|
+
expect(result.findings.map((f) => f.checkId)).not.toContain("SEC001");
|
|
535
|
+
rmSync(dir, { recursive: true, force: true });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test(".chant-audit.json's allowlist suppresses a finding by fingerprint, without storing the secret", async () => {
|
|
539
|
+
const dir = tmpRepo();
|
|
540
|
+
writeFileSync(join(dir, ".env"), `AWS_ACCESS_KEY_ID=${FAKE_AWS_KEY}\n`);
|
|
541
|
+
const fingerprint = fingerprintSecret(FAKE_AWS_KEY);
|
|
542
|
+
writeFileSync(join(dir, ".chant-audit.json"), JSON.stringify({ secrets: { allow: [{ ruleId: "SEC001", fingerprint }] } }));
|
|
543
|
+
const result = await auditCommand({ path: dir, plugins: [] });
|
|
544
|
+
expect(result.findings.map((f) => f.checkId)).not.toContain("SEC001");
|
|
545
|
+
rmSync(dir, { recursive: true, force: true });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("an explicit secretsScan option overrides (and wins over) the local config", async () => {
|
|
549
|
+
const dir = tmpRepo();
|
|
550
|
+
writeFileSync(join(dir, "config.yaml"), "token: kQ7mZ9pL2xR8vT4nW1sD6uJ3\n");
|
|
551
|
+
writeFileSync(join(dir, ".chant-audit.json"), JSON.stringify({ secrets: { entropyThreshold: 1 } }));
|
|
552
|
+
const result = await auditCommand({ path: dir, plugins: [], secretsScan: { entropyThreshold: 6.5 } });
|
|
553
|
+
expect(result.findings.map((f) => f.checkId)).not.toContain("SEC010");
|
|
554
|
+
rmSync(dir, { recursive: true, force: true });
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe("Wrangler config audit (#446)", () => {
|
|
559
|
+
function tmpRepo(): string {
|
|
560
|
+
const dir = join(tmpdir(), `chant-audit-wrangler-${process.pid}-${Math.random().toString(36).slice(2)}`);
|
|
561
|
+
mkdirSync(dir, { recursive: true });
|
|
562
|
+
return dir;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
test("audits wrangler.toml end to end, even when no lexicon is installed", async () => {
|
|
566
|
+
const dir = tmpRepo();
|
|
567
|
+
writeFileSync(
|
|
568
|
+
join(dir, "wrangler.toml"),
|
|
569
|
+
[
|
|
570
|
+
'name = "my-worker"',
|
|
571
|
+
"",
|
|
572
|
+
"[observability]",
|
|
573
|
+
"enabled = false",
|
|
574
|
+
"",
|
|
575
|
+
"[env.production]",
|
|
576
|
+
"workers_dev = true",
|
|
577
|
+
].join("\n"),
|
|
578
|
+
);
|
|
579
|
+
const result = await auditCommand({ path: dir, plugins: [], format: "json" });
|
|
580
|
+
expect(result.status).toBe("no-lexicons"); // no lexicon installed — still not a clean miss
|
|
581
|
+
const ids = result.findings.map((f) => f.checkId);
|
|
582
|
+
expect(ids).toContain("WRG001");
|
|
583
|
+
expect(ids).toContain("WRG003");
|
|
584
|
+
rmSync(dir, { recursive: true, force: true });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("a wrangler finding is tiered and cataloged like any other finding, alongside lexicon findings", async () => {
|
|
588
|
+
const dir = tmpRepo();
|
|
589
|
+
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
|
|
590
|
+
writeFileSync(
|
|
591
|
+
join(dir, ".github", "workflows", "ci.yml"),
|
|
592
|
+
"name: CI\non:\n push:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n",
|
|
593
|
+
);
|
|
594
|
+
writeFileSync(join(dir, "wrangler.toml"), 'routes = ["*/*"]\n');
|
|
595
|
+
const all = await loadAuditPlugins();
|
|
596
|
+
const result = await auditCommand({ path: dir, plugins: all.filter((p) => p.name === "github"), format: "json" });
|
|
597
|
+
expect(result.status).toBe("ok");
|
|
598
|
+
const json = JSON.parse(result.output);
|
|
599
|
+
const wrg = json.findings.find((f: { checkId: string }) => f.checkId === "WRG004");
|
|
600
|
+
expect(wrg).toBeDefined();
|
|
601
|
+
expect(wrg.tier).toBe("merge-worthy");
|
|
602
|
+
expect(wrg.category).toBe("security");
|
|
603
|
+
expect(wrg.file).toBe("wrangler.toml");
|
|
604
|
+
expect(wrg.docUrl).toBe("https://intentius.io/chant/lint-rules/audit-rules/#wrg004");
|
|
605
|
+
rmSync(dir, { recursive: true, force: true });
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("a malformed wrangler.toml contributes no findings, never fails the audit", async () => {
|
|
609
|
+
const dir = tmpRepo();
|
|
610
|
+
writeFileSync(join(dir, "wrangler.toml"), "not = valid = toml = [\n");
|
|
611
|
+
const result = await auditCommand({ path: dir, plugins: [] });
|
|
612
|
+
expect(result.success).toBe(true);
|
|
613
|
+
expect(result.findings.map((f) => f.checkId)).not.toEqual(expect.arrayContaining([expect.stringMatching(/^WRG/)]));
|
|
614
|
+
rmSync(dir, { recursive: true, force: true });
|
|
615
|
+
});
|
|
402
616
|
});
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* directly and runs the real post-synth checks via the audit core.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, statSync, writeFileSync } from "fs";
|
|
8
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
9
10
|
import { auditFiles, type AuditInput, type AuditFinding, type ChecksProvider } from "../../audit/core";
|
|
10
11
|
import { AUDIT_LEXICONS, classifyFiles, collectCandidates, loadAuditPlugins, unclaimedFiles, type DetectPlugin, type RepoFile, type UnclaimedFile } from "../../audit/discover";
|
|
11
12
|
import { RULE_CATALOG, resolveAuditCatalog, type RuleMeta } from "../../audit/catalog";
|
|
13
|
+
import { scanForSecrets, parseSecretsConfig, type SecretsScanOptions } from "../../audit/secrets";
|
|
14
|
+
import { auditWranglerConfigs } from "../../audit/wrangler";
|
|
12
15
|
import { renderMarkdown } from "../../audit/report";
|
|
13
16
|
import { renderHtml, type ReportTheme } from "../../audit/report-html";
|
|
14
17
|
import { buildReportJson, REPORT_SCHEMA_VERSION, type AuditSnapshot } from "../../audit/report-model";
|
|
@@ -47,6 +50,12 @@ export interface AuditCommandOptions {
|
|
|
47
50
|
toolVersion?: string;
|
|
48
51
|
/** Injectable detection plugins (testing); defaults to every installed audit lexicon. */
|
|
49
52
|
plugins?: DetectPlugin[];
|
|
53
|
+
/**
|
|
54
|
+
* Secrets-detection options (#443) — entropy threshold/min-length and an
|
|
55
|
+
* allowlist. Merged over a `.chant-audit.json` at the local target root, if
|
|
56
|
+
* present (URL targets have no local config file to read).
|
|
57
|
+
*/
|
|
58
|
+
secretsScan?: SecretsScanOptions;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export interface AuditCommandResult {
|
|
@@ -194,6 +203,26 @@ function sarifLevel(sev: Severity): string {
|
|
|
194
203
|
return sev === "error" ? "error" : sev === "warning" ? "warning" : "note";
|
|
195
204
|
}
|
|
196
205
|
|
|
206
|
+
/** `.chant-audit.json`'s `secrets` section at the local target root, if present (tolerant of a missing/malformed file). */
|
|
207
|
+
function readLocalSecretsConfig(root: string): SecretsScanOptions {
|
|
208
|
+
const file = join(root, ".chant-audit.json");
|
|
209
|
+
if (!existsSync(file)) return {};
|
|
210
|
+
try {
|
|
211
|
+
return parseSecretsConfig(readFileSync(file, "utf-8"));
|
|
212
|
+
} catch {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Merge the local `.chant-audit.json` config under any explicitly-passed options (options win). */
|
|
218
|
+
function resolveSecretsOptions(fileConfig: SecretsScanOptions, explicit?: SecretsScanOptions): SecretsScanOptions {
|
|
219
|
+
return {
|
|
220
|
+
entropyThreshold: explicit?.entropyThreshold ?? fileConfig.entropyThreshold,
|
|
221
|
+
entropyMinLength: explicit?.entropyMinLength ?? fileConfig.entropyMinLength,
|
|
222
|
+
allow: [...(fileConfig.allow ?? []), ...(explicit?.allow ?? [])],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
/** Coverage caveats about what the audit could and couldn't see. */
|
|
198
227
|
export function coverageNotes(inputs: AuditInput[]): string[] {
|
|
199
228
|
const notes: string[] = [];
|
|
@@ -221,7 +250,8 @@ function renderStylish(findings: AuditFinding[], scanned: string[], notes: strin
|
|
|
221
250
|
if (list.length === 0) return;
|
|
222
251
|
lines.push("", title);
|
|
223
252
|
for (const f of list) {
|
|
224
|
-
const
|
|
253
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
254
|
+
const where = f.entity ? `${loc} (${f.entity})` : loc;
|
|
225
255
|
const title = catalog[f.checkId]?.title ?? f.checkId;
|
|
226
256
|
lines.push(` [${f.checkId}] ${f.severity} ${where} — ${title}`);
|
|
227
257
|
}
|
|
@@ -231,6 +261,14 @@ function renderStylish(findings: AuditFinding[], scanned: string[], notes: strin
|
|
|
231
261
|
return lines.join("\n");
|
|
232
262
|
}
|
|
233
263
|
|
|
264
|
+
/**
|
|
265
|
+
* SARIF (2.1.0) export (#442). Rule-level metadata — category (the finding
|
|
266
|
+
* "dimension": security/correctness/best-practice) and remediation — is
|
|
267
|
+
* carried in the property bag / `help` block since SARIF's core schema has no
|
|
268
|
+
* dedicated slot for either; `tier` is per-result (a `RuleMeta` fact looked up
|
|
269
|
+
* per finding) so consumers can triage merge-worthy vs. report-only without a
|
|
270
|
+
* second catalog lookup.
|
|
271
|
+
*/
|
|
234
272
|
function renderSarif(findings: AuditFinding[], catalog: Record<string, RuleMeta> = RULE_CATALOG): string {
|
|
235
273
|
const ruleIds = [...new Set(findings.map((f) => f.checkId))].sort();
|
|
236
274
|
const rules = ruleIds.map((id) => {
|
|
@@ -239,15 +277,28 @@ function renderSarif(findings: AuditFinding[], catalog: Record<string, RuleMeta>
|
|
|
239
277
|
id,
|
|
240
278
|
name: m?.title ?? id,
|
|
241
279
|
shortDescription: { text: m?.title ?? id },
|
|
280
|
+
...(m?.remediation ? { help: { text: m.remediation } } : {}),
|
|
242
281
|
helpUri: m?.authority?.[0]?.url,
|
|
282
|
+
...(m?.category ? { properties: { category: m.category, dimension: m.category } } : {}),
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
const results = findings.map((f) => {
|
|
286
|
+
const tier = catalog[f.checkId]?.tier;
|
|
287
|
+
return {
|
|
288
|
+
ruleId: f.checkId,
|
|
289
|
+
level: sarifLevel(f.severity),
|
|
290
|
+
message: { text: f.message },
|
|
291
|
+
locations: [
|
|
292
|
+
{
|
|
293
|
+
physicalLocation: {
|
|
294
|
+
artifactLocation: { uri: f.file },
|
|
295
|
+
...(f.line ? { region: { startLine: f.line } } : {}),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
...(tier ? { properties: { tier } } : {}),
|
|
243
300
|
};
|
|
244
301
|
});
|
|
245
|
-
const results = findings.map((f) => ({
|
|
246
|
-
ruleId: f.checkId,
|
|
247
|
-
level: sarifLevel(f.severity),
|
|
248
|
-
message: { text: f.message },
|
|
249
|
-
locations: [{ physicalLocation: { artifactLocation: { uri: f.file } } }],
|
|
250
|
-
}));
|
|
251
302
|
return JSON.stringify(
|
|
252
303
|
{
|
|
253
304
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
@@ -327,28 +378,46 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
327
378
|
const unclaimed = unclaimedFiles(candidates, inputs, plugins);
|
|
328
379
|
const scanned = inputs.map((i) => i.path);
|
|
329
380
|
|
|
381
|
+
// Secrets detection (#443) is independent of lexicon: it scans every
|
|
382
|
+
// candidate file's raw text, not just the ones a lexicon claimed, and runs
|
|
383
|
+
// even when no lexicon is installed at all. `.chant-audit.json` at a local
|
|
384
|
+
// target's root supplies the entropy threshold / allowlist; a URL target
|
|
385
|
+
// has no local config file to read.
|
|
386
|
+
const secretsConfig = isUrl ? {} : readLocalSecretsConfig(options.path);
|
|
387
|
+
const secretsFindings = scanForSecrets(candidates, resolveSecretsOptions(secretsConfig, options.secretsScan));
|
|
388
|
+
// Wrangler config audit (#446) is likewise lexicon-independent: it scans
|
|
389
|
+
// every candidate file for `wrangler.toml` by its own detector, not through
|
|
390
|
+
// `classifyFiles`/an installed lexicon package (no such package exists —
|
|
391
|
+
// this format ships only a detector and checks, per the issue's scope).
|
|
392
|
+
const wranglerFindings = auditWranglerConfigs(candidates);
|
|
393
|
+
|
|
330
394
|
if (plugins.length === 0) {
|
|
331
395
|
const output = format === "json" ? renderNoLexiconsJson(options.path, unclaimed) : renderNoLexicons(options.path, unclaimed);
|
|
332
|
-
return { success: true, status: "no-lexicons", output, findings: [], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr" };
|
|
396
|
+
return { success: true, status: "no-lexicons", output, findings: [...secretsFindings, ...wranglerFindings], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr" };
|
|
333
397
|
}
|
|
334
398
|
|
|
335
399
|
const missingLexiconNote = missingLexiconHint(unclaimed);
|
|
336
400
|
|
|
337
|
-
if (inputs.length === 0) {
|
|
401
|
+
if (inputs.length === 0 && secretsFindings.length === 0 && wranglerFindings.length === 0) {
|
|
338
402
|
const output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
|
|
339
403
|
return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0 };
|
|
340
404
|
}
|
|
341
405
|
|
|
342
|
-
let findings: AuditFinding[];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
406
|
+
let findings: AuditFinding[] = [];
|
|
407
|
+
if (inputs.length > 0) {
|
|
408
|
+
try {
|
|
409
|
+
findings = await auditFiles(inputs, { checksProvider: options.checksProvider });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
412
|
+
return { success: false, output: "", findings: [], scanned, exitCode: 1, error: msg };
|
|
413
|
+
}
|
|
348
414
|
}
|
|
415
|
+
findings = [...findings, ...secretsFindings, ...wranglerFindings];
|
|
349
416
|
// Resolve the audit catalog once, aggregating the audited lexicons' own
|
|
350
417
|
// metadata over core's static catalog (#687). The lexicons are already loaded
|
|
351
|
-
// by `auditFiles` above, so this is cheap.
|
|
418
|
+
// by `auditFiles` above, so this is cheap. Core's static catalog (always
|
|
419
|
+
// included) carries the SEC* secrets rules, so they resolve even when
|
|
420
|
+
// `inputs` claimed nothing.
|
|
352
421
|
const catalog = await resolveAuditCatalog([...new Set(inputs.map((i) => i.lexicon))]);
|
|
353
422
|
|
|
354
423
|
if (tier === "merge-worthy") findings = findings.filter((f) => isMergeWorthy(f, catalog));
|
|
@@ -184,6 +184,71 @@ export const testEntity = {
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
test("#1083 — --fold-rank ranks blockers and, given a path, writes a collapsed-format export", async () => {
|
|
188
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
189
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
190
|
+
|
|
191
|
+
// A pair of files that fold fine (same shape as the #1022 test above),
|
|
192
|
+
// so the build produces output and the "no output" guard doesn't fire —
|
|
193
|
+
// unrelated to the ranking below.
|
|
194
|
+
await writeFile(
|
|
195
|
+
join(testDir, "resources.ts"),
|
|
196
|
+
`
|
|
197
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
198
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
199
|
+
`,
|
|
200
|
+
);
|
|
201
|
+
await writeFile(
|
|
202
|
+
join(testDir, "main.ts"),
|
|
203
|
+
`
|
|
204
|
+
import { Bucket } from "./resources";
|
|
205
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
206
|
+
`,
|
|
207
|
+
);
|
|
208
|
+
// a.ts fails directly (process.env); b.ts fails because it imports a.ts.
|
|
209
|
+
await writeFile(join(testDir, "a.ts"), `export const a = process.env.SOMETHING;`);
|
|
210
|
+
await writeFile(
|
|
211
|
+
join(testDir, "b.ts"),
|
|
212
|
+
`
|
|
213
|
+
import { a } from "./a";
|
|
214
|
+
export const b = a;
|
|
215
|
+
`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const awsSerializer: Serializer = {
|
|
219
|
+
name: "aws",
|
|
220
|
+
rulePrefix: "TEST",
|
|
221
|
+
serialize: (entities) => JSON.stringify([...entities.keys()]),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const collapsedPath = join(testDir, "out", "fold.collapsed");
|
|
225
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
226
|
+
try {
|
|
227
|
+
const result = await buildCommand({
|
|
228
|
+
path: testDir,
|
|
229
|
+
format: "json",
|
|
230
|
+
serializers: [awsSerializer],
|
|
231
|
+
fold: true,
|
|
232
|
+
foldRank: true,
|
|
233
|
+
foldRankCollapsedFile: collapsedPath,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.success).toBe(true);
|
|
237
|
+
|
|
238
|
+
const lines = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
239
|
+
const rankingLine = lines.find((l) => l.includes("fold-rank:") && l.includes("blocker(s)"));
|
|
240
|
+
expect(rankingLine).toBeDefined();
|
|
241
|
+
const aLine = lines.find((l) => l.includes("retained 2") && l.includes("a.ts"));
|
|
242
|
+
expect(aLine).toBeDefined();
|
|
243
|
+
|
|
244
|
+
expect(existsSync(collapsedPath)).toBe(true);
|
|
245
|
+
const collapsed = readFileSync(collapsedPath, "utf-8").trim().split("\n");
|
|
246
|
+
expect(collapsed.sort()).toEqual(["fold;a.ts 1", "fold;a.ts;b.ts 1"].sort());
|
|
247
|
+
} finally {
|
|
248
|
+
errorSpy.mockRestore();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
187
252
|
describe("#1396 — ownership.env resolves from the env build parameter", () => {
|
|
188
253
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
189
254
|
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
@@ -712,7 +777,11 @@ export const testEntity = {
|
|
|
712
777
|
const sidecarSerializer: Serializer = {
|
|
713
778
|
name: "multi",
|
|
714
779
|
rulePrefix: "MULTI",
|
|
715
|
-
serialize: () => ({
|
|
780
|
+
serialize: () => ({
|
|
781
|
+
primary: "kind: Deployment\n",
|
|
782
|
+
files: { "db.sops.yaml": CIPHERTEXT },
|
|
783
|
+
verbatimFiles: ["db.sops.yaml"],
|
|
784
|
+
}),
|
|
716
785
|
};
|
|
717
786
|
|
|
718
787
|
async function writeProject(fileValue: string): Promise<void> {
|
|
@@ -768,7 +837,11 @@ export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexi
|
|
|
768
837
|
const jsonSidecar: Serializer = {
|
|
769
838
|
name: "multi",
|
|
770
839
|
rulePrefix: "MULTI",
|
|
771
|
-
serialize: () => ({
|
|
840
|
+
serialize: () => ({
|
|
841
|
+
primary: "kind: Deployment\n",
|
|
842
|
+
files: { "db.sops.yaml": jsonish },
|
|
843
|
+
verbatimFiles: ["db.sops.yaml"],
|
|
844
|
+
}),
|
|
772
845
|
};
|
|
773
846
|
await writeProject("secrets/db.sops.yaml");
|
|
774
847
|
const outputPath = join(testDir, "dist", "manifests.yaml");
|
|
@@ -785,6 +858,98 @@ export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexi
|
|
|
785
858
|
});
|
|
786
859
|
});
|
|
787
860
|
|
|
861
|
+
describe("SerializerResult.verbatimFiles — general channel (#1937)", () => {
|
|
862
|
+
test("a verbatim file whose content happens to be valid JSON survives byte-identical", async () => {
|
|
863
|
+
// Deliberately not key-sorted or 2-space indented — content the JSON
|
|
864
|
+
// round trip below would reformat. The old workaround (skip the round
|
|
865
|
+
// trip only because a file happens to fail JSON.parse, true for the
|
|
866
|
+
// committed-encrypted `.sops.yaml` case but never a real guarantee)
|
|
867
|
+
// could not protect this case. `verbatimFiles` can, structurally.
|
|
868
|
+
const raw = '{"b":1,"a":2, "nested":{"z":true}}';
|
|
869
|
+
const verbatimSerializer: Serializer = {
|
|
870
|
+
name: "multi",
|
|
871
|
+
rulePrefix: "MULTI",
|
|
872
|
+
serialize: () => ({
|
|
873
|
+
primary: "{}",
|
|
874
|
+
files: { "raw.json": raw },
|
|
875
|
+
verbatimFiles: ["raw.json"],
|
|
876
|
+
}),
|
|
877
|
+
};
|
|
878
|
+
await writeFile(
|
|
879
|
+
join(testDir, "infra.ts"),
|
|
880
|
+
`export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexicon: "multi", kind: "resource", props: {}, attributes: {} };`,
|
|
881
|
+
);
|
|
882
|
+
const outputPath = join(testDir, "dist", "manifest.json");
|
|
883
|
+
|
|
884
|
+
const result = await buildCommand({
|
|
885
|
+
path: testDir,
|
|
886
|
+
output: outputPath,
|
|
887
|
+
format: "json",
|
|
888
|
+
serializers: [verbatimSerializer],
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
expect(result.errors).toEqual([]);
|
|
892
|
+
expect(readFileSync(join(testDir, "dist", "raw.json"), "utf-8")).toBe(raw);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
describe("additional-file basename collisions across lexicons (#1937)", () => {
|
|
897
|
+
const lexASerializer = (content: string): Serializer => ({
|
|
898
|
+
name: "lexa",
|
|
899
|
+
rulePrefix: "LEXA",
|
|
900
|
+
serialize: () => ({ primary: "{}", files: { "shared.yaml": content } }),
|
|
901
|
+
});
|
|
902
|
+
const lexBSerializer = (content: string): Serializer => ({
|
|
903
|
+
name: "lexb",
|
|
904
|
+
rulePrefix: "LEXB",
|
|
905
|
+
serialize: () => ({ primary: "{}", files: { "shared.yaml": content } }),
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
async function writeTwoLexiconProject(): Promise<void> {
|
|
909
|
+
await writeFile(
|
|
910
|
+
join(testDir, "infra.ts"),
|
|
911
|
+
[
|
|
912
|
+
`export const a = { [Symbol.for("chant.declarable")]: true, entityType: "A", lexicon: "lexa", kind: "resource", props: {}, attributes: {} };`,
|
|
913
|
+
`export const b = { [Symbol.for("chant.declarable")]: true, entityType: "B", lexicon: "lexb", kind: "resource", props: {}, attributes: {} };`,
|
|
914
|
+
].join("\n"),
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
test("differing content at the same basename fails the build, naming the basename and both sources", async () => {
|
|
919
|
+
await writeTwoLexiconProject();
|
|
920
|
+
const outputPath = join(testDir, "dist", "manifest.json");
|
|
921
|
+
|
|
922
|
+
const result = await buildCommand({
|
|
923
|
+
path: testDir,
|
|
924
|
+
output: outputPath,
|
|
925
|
+
format: "json",
|
|
926
|
+
serializers: [lexASerializer("from lexa\n"), lexBSerializer("from lexb\n")],
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
expect(result.success).toBe(false);
|
|
930
|
+
const message = result.errors.join("\n");
|
|
931
|
+
expect(message).toContain("shared.yaml");
|
|
932
|
+
expect(message).toContain("lexa");
|
|
933
|
+
expect(message).toContain("lexb");
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test("identical content at the same basename passes", async () => {
|
|
937
|
+
await writeTwoLexiconProject();
|
|
938
|
+
const outputPath = join(testDir, "dist", "manifest.json");
|
|
939
|
+
const shared = "same content for both\n";
|
|
940
|
+
|
|
941
|
+
const result = await buildCommand({
|
|
942
|
+
path: testDir,
|
|
943
|
+
output: outputPath,
|
|
944
|
+
format: "json",
|
|
945
|
+
serializers: [lexASerializer(shared), lexBSerializer(shared)],
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
expect(result.errors).toEqual([]);
|
|
949
|
+
expect(readFileSync(join(testDir, "dist", "shared.yaml"), "utf-8")).toBe(shared);
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
|
|
788
953
|
test("op worker files go to <project>/dist/ops even with no --output (#878)", async () => {
|
|
789
954
|
// The generated Op worker must land where `chant run <op> --temporal` reads it
|
|
790
955
|
// (`<project>/dist/ops/<name>/worker.ts`) even when the build has no --output
|