@intentius/chant 0.49.0 → 0.50.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.
Files changed (247) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +9 -0
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +6 -0
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/cli/commands/audit.d.ts +7 -0
  19. package/dist/cli/commands/audit.d.ts.map +1 -1
  20. package/dist/cli/commands/build.d.ts +23 -0
  21. package/dist/cli/commands/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/components.d.ts +31 -0
  24. package/dist/cli/handlers/components.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts +11 -0
  26. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  27. package/dist/cli/handlers/operator.d.ts +32 -0
  28. package/dist/cli/handlers/operator.d.ts.map +1 -0
  29. package/dist/cli/handlers/scenario.d.ts +39 -0
  30. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/mcp/server.d.ts +35 -2
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/types.d.ts +29 -1
  35. package/dist/cli/mcp/types.d.ts.map +1 -1
  36. package/dist/cli/registry.d.ts +14 -2
  37. package/dist/cli/registry.d.ts.map +1 -1
  38. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  39. package/dist/components/capability.d.ts +17 -2
  40. package/dist/components/capability.d.ts.map +1 -1
  41. package/dist/components/cli-support.d.ts +7 -0
  42. package/dist/components/cli-support.d.ts.map +1 -1
  43. package/dist/components/component.d.ts +15 -0
  44. package/dist/components/component.d.ts.map +1 -1
  45. package/dist/components/driver.d.ts.map +1 -1
  46. package/dist/components/verbs/index.d.ts +6 -1
  47. package/dist/components/verbs/index.d.ts.map +1 -1
  48. package/dist/components/verbs/run-agent.d.ts +499 -0
  49. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  50. package/dist/components/verbs/sign.d.ts +30 -0
  51. package/dist/components/verbs/sign.d.ts.map +1 -1
  52. package/dist/composite.d.ts +6 -1
  53. package/dist/composite.d.ts.map +1 -1
  54. package/dist/discovery/collect.d.ts.map +1 -1
  55. package/dist/discovery/fold-import.d.ts +15 -1
  56. package/dist/discovery/fold-import.d.ts.map +1 -1
  57. package/dist/discovery/fold-rank.d.ts +66 -0
  58. package/dist/discovery/fold-rank.d.ts.map +1 -0
  59. package/dist/discovery/index.d.ts +15 -0
  60. package/dist/discovery/index.d.ts.map +1 -1
  61. package/dist/discovery/param-deps.d.ts +17 -0
  62. package/dist/discovery/param-deps.d.ts.map +1 -0
  63. package/dist/fold/fold.d.ts +55 -2
  64. package/dist/fold/fold.d.ts.map +1 -1
  65. package/dist/fold/subset.d.ts +21 -14
  66. package/dist/fold/subset.d.ts.map +1 -1
  67. package/dist/lexicon-schema.d.ts +2 -0
  68. package/dist/lexicon-schema.d.ts.map +1 -1
  69. package/dist/lexicon.d.ts +93 -0
  70. package/dist/lexicon.d.ts.map +1 -1
  71. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  72. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  73. package/dist/lifecycle/deep-diff.d.ts +18 -0
  74. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  75. package/dist/lifecycle/deep-observe.d.ts +9 -1
  76. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  77. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  78. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  79. package/dist/lifecycle/git.d.ts +145 -21
  80. package/dist/lifecycle/git.d.ts.map +1 -1
  81. package/dist/lifecycle/index.d.ts +4 -0
  82. package/dist/lifecycle/index.d.ts.map +1 -1
  83. package/dist/lifecycle/lease.d.ts +113 -0
  84. package/dist/lifecycle/lease.d.ts.map +1 -0
  85. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  86. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  87. package/dist/lifecycle/scenario.d.ts +163 -0
  88. package/dist/lifecycle/scenario.d.ts.map +1 -0
  89. package/dist/lifecycle/symptoms.d.ts +63 -0
  90. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  91. package/dist/lint/output-docs.d.ts +94 -0
  92. package/dist/lint/output-docs.d.ts.map +1 -0
  93. package/dist/lint/post-synth.d.ts +29 -0
  94. package/dist/lint/post-synth.d.ts.map +1 -1
  95. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  96. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  97. package/dist/lsp/lexicon-providers.d.ts +7 -0
  98. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  99. package/dist/op/activity-contract.d.ts +139 -0
  100. package/dist/op/activity-contract.d.ts.map +1 -0
  101. package/dist/op/builders.d.ts +42 -2
  102. package/dist/op/builders.d.ts.map +1 -1
  103. package/dist/op/converge-rule.d.ts +161 -0
  104. package/dist/op/converge-rule.d.ts.map +1 -0
  105. package/dist/op/generate-pipeline.d.ts +39 -0
  106. package/dist/op/generate-pipeline.d.ts.map +1 -0
  107. package/dist/op/index.d.ts +14 -0
  108. package/dist/op/index.d.ts.map +1 -1
  109. package/dist/op/local-executor.d.ts.map +1 -1
  110. package/dist/op/op-verb-class.d.ts +42 -0
  111. package/dist/op/op-verb-class.d.ts.map +1 -0
  112. package/dist/op/operator.d.ts +128 -0
  113. package/dist/op/operator.d.ts.map +1 -0
  114. package/dist/op/step-output-ref.d.ts +187 -0
  115. package/dist/op/step-output-ref.d.ts.map +1 -0
  116. package/dist/op/types.d.ts +18 -1
  117. package/dist/op/types.d.ts.map +1 -1
  118. package/dist/provenance.d.ts +73 -3
  119. package/dist/provenance.d.ts.map +1 -1
  120. package/dist/runtime-adapter.d.ts +7 -1
  121. package/dist/runtime-adapter.d.ts.map +1 -1
  122. package/dist/serializer.d.ts +18 -0
  123. package/dist/serializer.d.ts.map +1 -1
  124. package/dist/toml.d.ts +40 -5
  125. package/dist/toml.d.ts.map +1 -1
  126. package/package.json +1 -1
  127. package/src/audit/catalog.test.ts +1 -1
  128. package/src/audit/catalog.ts +75 -3
  129. package/src/audit/core.ts +9 -0
  130. package/src/audit/discover.ts +29 -2
  131. package/src/audit/fetch.test.ts +216 -3
  132. package/src/audit/fetch.ts +270 -59
  133. package/src/audit/report-html.ts +5 -2
  134. package/src/audit/report-model.ts +9 -0
  135. package/src/audit/report.test.ts +22 -0
  136. package/src/audit/report.ts +3 -2
  137. package/src/audit/rules-doc.ts +2 -0
  138. package/src/audit/secrets.test.ts +303 -0
  139. package/src/audit/secrets.ts +406 -0
  140. package/src/audit/wrangler.test.ts +230 -0
  141. package/src/audit/wrangler.ts +290 -0
  142. package/src/build.ts +8 -3
  143. package/src/cli/command-group.ts +1 -1
  144. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  145. package/src/cli/commands/audit.test.ts +215 -1
  146. package/src/cli/commands/audit.ts +86 -17
  147. package/src/cli/commands/build.test.ts +167 -2
  148. package/src/cli/commands/build.ts +114 -23
  149. package/src/cli/handlers/build.ts +2 -0
  150. package/src/cli/handlers/components.test.ts +199 -1
  151. package/src/cli/handlers/components.ts +160 -3
  152. package/src/cli/handlers/graph.test.ts +20 -0
  153. package/src/cli/handlers/graph.ts +10 -1
  154. package/src/cli/handlers/lifecycle.ts +12 -4
  155. package/src/cli/handlers/operator.test.ts +255 -0
  156. package/src/cli/handlers/operator.ts +240 -0
  157. package/src/cli/handlers/scenario.test.ts +456 -0
  158. package/src/cli/handlers/scenario.ts +330 -0
  159. package/src/cli/main.test.ts +23 -0
  160. package/src/cli/main.ts +72 -1
  161. package/src/cli/mcp/server.test.ts +265 -2
  162. package/src/cli/mcp/server.ts +84 -7
  163. package/src/cli/mcp/types.ts +27 -1
  164. package/src/cli/registry.ts +14 -2
  165. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  166. package/src/codegen/docs-rule-scanning.ts +25 -2
  167. package/src/components/README.md +7 -0
  168. package/src/components/capability.ts +17 -2
  169. package/src/components/cli-support.test.ts +17 -0
  170. package/src/components/cli-support.ts +13 -1
  171. package/src/components/component-schema.test.ts +32 -0
  172. package/src/components/component.schema.json +6 -0
  173. package/src/components/component.test.ts +21 -0
  174. package/src/components/component.ts +15 -0
  175. package/src/components/driver.ts +12 -4
  176. package/src/components/verbs/index.ts +6 -1
  177. package/src/components/verbs/run-agent.test.ts +683 -0
  178. package/src/components/verbs/run-agent.ts +786 -0
  179. package/src/components/verbs/sign.test.ts +19 -0
  180. package/src/components/verbs/sign.ts +34 -2
  181. package/src/composite.ts +31 -2
  182. package/src/discovery/collect.ts +11 -2
  183. package/src/discovery/fold-import.test.ts +54 -0
  184. package/src/discovery/fold-import.ts +178 -38
  185. package/src/discovery/fold-rank.test.ts +197 -0
  186. package/src/discovery/fold-rank.ts +346 -0
  187. package/src/discovery/index.ts +16 -1
  188. package/src/discovery/param-deps.test.ts +118 -0
  189. package/src/discovery/param-deps.ts +170 -0
  190. package/src/fold/fold.test.ts +6 -2
  191. package/src/fold/fold.ts +184 -3
  192. package/src/fold/subset.test.ts +82 -19
  193. package/src/fold/subset.ts +79 -41
  194. package/src/lexicon-schema.ts +3 -0
  195. package/src/lexicon.ts +103 -2
  196. package/src/lifecycle/converge-ledger.test.ts +199 -0
  197. package/src/lifecycle/converge-ledger.ts +179 -0
  198. package/src/lifecycle/deep-diff.test.ts +79 -1
  199. package/src/lifecycle/deep-diff.ts +23 -0
  200. package/src/lifecycle/deep-observe.ts +13 -2
  201. package/src/lifecycle/gate-ledger.test.ts +103 -0
  202. package/src/lifecycle/gate-ledger.ts +140 -0
  203. package/src/lifecycle/git.test.ts +430 -0
  204. package/src/lifecycle/git.ts +446 -84
  205. package/src/lifecycle/index.ts +4 -0
  206. package/src/lifecycle/lease.test.ts +343 -0
  207. package/src/lifecycle/lease.ts +270 -0
  208. package/src/lifecycle/scenario-eval.test.ts +199 -0
  209. package/src/lifecycle/scenario-eval.ts +158 -0
  210. package/src/lifecycle/scenario.test.ts +195 -0
  211. package/src/lifecycle/scenario.ts +321 -0
  212. package/src/lifecycle/symptoms.test.ts +116 -0
  213. package/src/lifecycle/symptoms.ts +126 -0
  214. package/src/lint/output-docs.test.ts +220 -0
  215. package/src/lint/output-docs.ts +204 -0
  216. package/src/lint/post-synth.test.ts +97 -0
  217. package/src/lint/post-synth.ts +45 -0
  218. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  219. package/src/lint/rules/comp/comp.test.ts +49 -1
  220. package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
  221. package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
  222. package/src/lsp/lexicon-providers.test.ts +44 -0
  223. package/src/lsp/lexicon-providers.ts +11 -1
  224. package/src/op/activity-contract.test.ts +180 -0
  225. package/src/op/activity-contract.ts +278 -0
  226. package/src/op/builders-exports.test.ts +17 -1
  227. package/src/op/builders.ts +59 -5
  228. package/src/op/converge-rule.test.ts +179 -0
  229. package/src/op/converge-rule.ts +311 -0
  230. package/src/op/generate-pipeline.test.ts +53 -0
  231. package/src/op/generate-pipeline.ts +99 -0
  232. package/src/op/index.ts +30 -0
  233. package/src/op/local-executor.test.ts +92 -0
  234. package/src/op/local-executor.ts +45 -9
  235. package/src/op/op-verb-class.test.ts +126 -0
  236. package/src/op/op-verb-class.ts +115 -0
  237. package/src/op/operator.test.ts +346 -0
  238. package/src/op/operator.ts +213 -0
  239. package/src/op/step-output-ref.test.ts +334 -0
  240. package/src/op/step-output-ref.ts +453 -0
  241. package/src/op/types.ts +18 -1
  242. package/src/provenance.test.ts +151 -4
  243. package/src/provenance.ts +118 -4
  244. package/src/runtime-adapter.ts +31 -10
  245. package/src/serializer.ts +18 -0
  246. package/src/toml.test.ts +157 -384
  247. package/src/toml.ts +371 -5
@@ -0,0 +1,303 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { scanForSecrets, fingerprintSecret, parseSecretsConfig, looksLikeIdentifierSlug, ENTROPY_RULE_ID, type ScannableFile } from "./secrets";
3
+
4
+ // Deliberately fake, non-functional values shaped like each detector's
5
+ // pattern (so the regexes under test have something real to match). Built via
6
+ // string concatenation — never a contiguous literal in this file's raw bytes
7
+ // — so pushing this file doesn't trip GitHub's own secret-scanning push
8
+ // protection on what are just test fixtures.
9
+ const FAKE_AWS_KEY = "AKIA" + "ABCDEFGHIJKLMNOP";
10
+ const FAKE_AWS_SECRET = "aB3dEfGhIjKlMnOpQrStUvWxYz01234567" + "89+/Q9";
11
+ const FAKE_GH_TOKEN = "ghp_" + "a".repeat(20) + "B1C2D3E4F5G6H7I8J9K0";
12
+ const FAKE_SLACK_TOKEN = "xoxb-123456789012-1234567890123-" + "abcdefghijklmnopqrstuvwx";
13
+ const FAKE_GOOGLE_KEY = "AIza" + "Sy" + "A".repeat(33);
14
+ const FAKE_STRIPE_LIVE = "sk_live_" + "4eC39HqLyjWDarjtT1zdp7dc";
15
+ const FAKE_STRIPE_TEST = "sk_test_" + "4eC39HqLyjWDarjtT1zdp7dc";
16
+
17
+ function ids(files: ScannableFile[], opts?: Parameters<typeof scanForSecrets>[1]): string[] {
18
+ return scanForSecrets(files, opts).map((f) => f.checkId);
19
+ }
20
+
21
+ describe("scanForSecrets — pattern detectors (positive + negative)", () => {
22
+ test("SEC001: AWS access key ID", () => {
23
+ const hit = FAKE_AWS_KEY;
24
+ expect(ids([{ path: "a.env", content: `KEY=${hit}\n` }])).toContain("SEC001");
25
+ // AWS's own documented example key is a well-known placeholder.
26
+ expect(ids([{ path: "a.env", content: "KEY=AKIAIOSFODNN7EXAMPLE\n" }])).not.toContain("SEC001");
27
+ });
28
+
29
+ test("SEC002: AWS secret access key (contextual)", () => {
30
+ const hit = `aws_secret_access_key = "${FAKE_AWS_SECRET}"`;
31
+ expect(ids([{ path: "creds.ini", content: hit }])).toContain("SEC002");
32
+ // The AWS docs' own example secret key contains "EXAMPLE" — a placeholder.
33
+ expect(ids([{ path: "creds.ini", content: 'aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' }])).not.toContain("SEC002");
34
+ // A reference, not a literal, is not a secret at all.
35
+ expect(ids([{ path: "creds.ini", content: "aws_secret_access_key = ${AWS_SECRET}" }])).not.toContain("SEC002");
36
+ });
37
+
38
+ test("SEC003: GitHub token", () => {
39
+ const hit = FAKE_GH_TOKEN;
40
+ expect(ids([{ path: "notes.txt", content: hit }])).toContain("SEC003");
41
+ expect(ids([{ path: "notes.txt", content: "ghp_ this is not a token, just prose" }])).not.toContain("SEC003");
42
+ });
43
+
44
+ test("SEC004: Slack token", () => {
45
+ const hit = FAKE_SLACK_TOKEN;
46
+ expect(ids([{ path: "notes.txt", content: hit }])).toContain("SEC004");
47
+ expect(ids([{ path: "notes.txt", content: "no slack token here" }])).not.toContain("SEC004");
48
+ });
49
+
50
+ test("SEC005: Google API key", () => {
51
+ const hit = FAKE_GOOGLE_KEY;
52
+ expect(hit.length).toBe(39);
53
+ expect(ids([{ path: "config.json", content: `{"key": "${hit}"}` }])).toContain("SEC005");
54
+ expect(ids([{ path: "config.json", content: '{"key": "not-a-google-key"}' }])).not.toContain("SEC005");
55
+ });
56
+
57
+ test("SEC006: Stripe live secret key", () => {
58
+ const hit = FAKE_STRIPE_LIVE;
59
+ expect(ids([{ path: ".env", content: `STRIPE_KEY=${hit}\n` }])).toContain("SEC006");
60
+ // Test-mode keys are out of scope for this rule.
61
+ expect(ids([{ path: ".env", content: `STRIPE_KEY=${FAKE_STRIPE_TEST}\n` }])).not.toContain("SEC006");
62
+ });
63
+
64
+ test("SEC007: PEM private key block", () => {
65
+ const pem = [
66
+ "-----BEGIN RSA PRIVATE KEY-----",
67
+ "MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aumqCp9nkc/AI5wpN0/f9YkFxx",
68
+ "wPqDgCUvo3PZ4d1JXCyv6l3lRb8lgSVeYjfXOsi9NxWTeR/xdz1Tv7DGwmlfjO7X",
69
+ "-----END RSA PRIVATE KEY-----",
70
+ ].join("\n");
71
+ expect(ids([{ path: "id_rsa", content: pem }])).toContain("SEC007");
72
+ expect(ids([{ path: "id_rsa.pub", content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC user@host" }])).not.toContain("SEC007");
73
+ });
74
+
75
+ test("SEC008: bearer/authorization token", () => {
76
+ const hit = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abc123def456ghi789";
77
+ expect(ids([{ path: "curl.sh", content: hit }])).toContain("SEC008");
78
+ expect(ids([{ path: "curl.sh", content: "Authorization: Bearer ${TOKEN}" }])).not.toContain("SEC008");
79
+ });
80
+
81
+ test("SEC009: credentials embedded in a connection string", () => {
82
+ const hit = "postgres://appuser:S7fkq2!zXpLm9@db.internal:5432/prod";
83
+ expect(ids([{ path: "settings.py", content: `DATABASE_URL = "${hit}"` }])).toContain("SEC009");
84
+ // Bland placeholder creds are excluded.
85
+ expect(ids([{ path: "settings.py", content: 'DATABASE_URL = "postgres://user:password@localhost:5432/dev"' }])).not.toContain("SEC009");
86
+ });
87
+
88
+ test("SEC010: high-entropy catch-all, and does not double-flag a pattern-matched value", () => {
89
+ const random = "kQ7mZ9pL2xR8vT4nW1sD6uJ3";
90
+ expect(ids([{ path: "notes.txt", content: `token = ${random}` }], { entropyMinLength: 20 })).toContain(ENTROPY_RULE_ID);
91
+ // A git-sha-shaped pure-hex run is not flagged (common false positive source).
92
+ expect(ids([{ path: "lock.json", content: "resolved-commit: 4b825dc642cb6eb9a060e54bf8d69288fbee4904" }], { entropyMinLength: 20 })).not.toContain(
93
+ ENTROPY_RULE_ID,
94
+ );
95
+ // Already caught by a pattern detector — the entropy pass must not also fire on the same text.
96
+ const findings = scanForSecrets([{ path: "a.env", content: `KEY=${FAKE_AWS_KEY}` }], { entropyMinLength: 10 });
97
+ expect(findings.filter((f) => f.checkId === ENTROPY_RULE_ID)).toHaveLength(0);
98
+ });
99
+ });
100
+
101
+ describe("entropy heuristic tunability (#443)", () => {
102
+ const content = "token = kQ7mZ9pL2xR8vT4nW1sD6uJ3";
103
+
104
+ test("default threshold catches the high-entropy token", () => {
105
+ expect(ids([{ path: "f.txt", content }])).toContain(ENTROPY_RULE_ID);
106
+ });
107
+
108
+ test("raising the threshold suppresses it", () => {
109
+ expect(ids([{ path: "f.txt", content }], { entropyThreshold: 6.5 })).not.toContain(ENTROPY_RULE_ID);
110
+ });
111
+
112
+ test("raising the minimum length excludes short candidate tokens", () => {
113
+ expect(ids([{ path: "f.txt", content }], { entropyMinLength: 100 })).not.toContain(ENTROPY_RULE_ID);
114
+ });
115
+ });
116
+
117
+ describe("entropy heuristic — identifier/slug false-positive guard (#443)", () => {
118
+ test("a dashed/slashed identifier (model id, package spec, image ref) is not flagged", () => {
119
+ expect(ids([{ path: "fleet.yaml", content: "model: anthropic/claude-sonnet-4-6\n" }])).not.toContain(ENTROPY_RULE_ID);
120
+ expect(ids([{ path: "package.json", content: '"some-package-name": "acme-widget-factory-lib"' }])).not.toContain(ENTROPY_RULE_ID);
121
+ });
122
+
123
+ test("a UUID-shaped value (hyphenated but not dictionary words) is still a candidate", () => {
124
+ // Not asserting it fires (entropy of hex digits alone may sit under the default
125
+ // threshold) — only that the slug guard doesn't blanket-exempt hyphenated values.
126
+ expect(looksLikeIdentifierSlug("550e8400-e29b-41d4-a716-446655440000")).toBe(false);
127
+ });
128
+ });
129
+
130
+ describe("redaction guarantee (#443)", () => {
131
+ test("the matched secret value never appears anywhere in a finding", () => {
132
+ const secret = FAKE_AWS_KEY;
133
+ const findings = scanForSecrets([{ path: "a.env", content: `AWS_ACCESS_KEY_ID=${secret}\n` }]);
134
+ expect(findings.length).toBeGreaterThan(0);
135
+ for (const f of findings) {
136
+ const serialized = JSON.stringify(f);
137
+ expect(serialized).not.toContain(secret);
138
+ expect(f.message).not.toContain(secret);
139
+ expect(f.entity ?? "").not.toContain(secret);
140
+ }
141
+ });
142
+
143
+ test("holds across every detector, including entropy and PEM blocks", () => {
144
+ const pemBody = "MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aumqCp9nkc/AI5wpN0/f9YkFxx";
145
+ const secrets = [FAKE_AWS_KEY, FAKE_GH_TOKEN, FAKE_SLACK_TOKEN, FAKE_GOOGLE_KEY, FAKE_STRIPE_LIVE, pemBody, "kQ7mZ9pL2xR8vT4nW1sD6uJ3"];
146
+ const content = [
147
+ FAKE_AWS_KEY,
148
+ FAKE_GH_TOKEN,
149
+ FAKE_SLACK_TOKEN,
150
+ FAKE_GOOGLE_KEY,
151
+ FAKE_STRIPE_LIVE,
152
+ `-----BEGIN RSA PRIVATE KEY-----`,
153
+ pemBody,
154
+ `-----END RSA PRIVATE KEY-----`,
155
+ `token = kQ7mZ9pL2xR8vT4nW1sD6uJ3`,
156
+ `postgres://appuser:S7fkq2!zXpLm9@db.internal:5432/prod`,
157
+ ].join("\n");
158
+
159
+ const findings = scanForSecrets([{ path: "big.env", content }], { entropyMinLength: 20 });
160
+ expect(findings.length).toBeGreaterThan(5);
161
+ const dump = JSON.stringify(findings);
162
+ for (const secret of secrets) expect(dump).not.toContain(secret);
163
+ expect(dump).not.toContain("appuser:S7fkq2!zXpLm9");
164
+ });
165
+
166
+ test("a finding carries a fingerprint and file/line location instead of the value", () => {
167
+ const [finding] = scanForSecrets([{ path: "a.env", content: `\n\nAWS=${FAKE_AWS_KEY}\n` }]);
168
+ expect(finding.file).toBe("a.env");
169
+ expect(finding.line).toBe(3);
170
+ expect(finding.fingerprint).toMatch(/^[0-9a-f]{16}$/);
171
+ });
172
+ });
173
+
174
+ describe("fingerprintSecret", () => {
175
+ test("is deterministic and does not echo the input", () => {
176
+ const fp1 = fingerprintSecret("super-secret-value");
177
+ const fp2 = fingerprintSecret("super-secret-value");
178
+ expect(fp1).toBe(fp2);
179
+ expect(fp1).not.toContain("super-secret-value");
180
+ expect(fp1).toMatch(/^[0-9a-f]{16}$/);
181
+ });
182
+
183
+ test("different inputs produce different fingerprints", () => {
184
+ expect(fingerprintSecret("a")).not.toBe(fingerprintSecret("b"));
185
+ });
186
+ });
187
+
188
+ describe("inline ignore marker (#443)", () => {
189
+ const secret = FAKE_AWS_KEY;
190
+
191
+ test("a same-line marker suppresses the finding", () => {
192
+ const content = `AWS=${secret} # chant-audit-ignore\n`;
193
+ expect(ids([{ path: "a.env", content }])).toEqual([]);
194
+ });
195
+
196
+ test("a marker scoped to a different rule id does not suppress", () => {
197
+ const content = `AWS=${secret} # chant-audit-ignore: SEC099\n`;
198
+ expect(ids([{ path: "a.env", content }])).toContain("SEC001");
199
+ });
200
+
201
+ test("a marker scoped to the matching rule id suppresses", () => {
202
+ const content = `AWS=${secret} # chant-audit-ignore: SEC001\n`;
203
+ expect(ids([{ path: "a.env", content }])).toEqual([]);
204
+ });
205
+
206
+ test("a marker on the line above suppresses (for blocks that can't carry a trailing comment)", () => {
207
+ const pem = ["# chant-audit-ignore: SEC007", "-----BEGIN RSA PRIVATE KEY-----", "MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aumqCp9nkc", "-----END RSA PRIVATE KEY-----"].join(
208
+ "\n",
209
+ );
210
+ expect(ids([{ path: "id_rsa", content: pem }])).toEqual([]);
211
+ });
212
+
213
+ test("without a marker, the finding is not suppressed", () => {
214
+ expect(ids([{ path: "a.env", content: `AWS=${secret}\n` }])).toContain("SEC001");
215
+ });
216
+ });
217
+
218
+ describe("config allowlist (#443)", () => {
219
+ const secret = FAKE_AWS_KEY;
220
+ const content = `AWS=${secret}\n`;
221
+
222
+ test("an allow entry matching ruleId + fingerprint suppresses the finding", () => {
223
+ const fingerprint = fingerprintSecret(secret);
224
+ const findings = scanForSecrets([{ path: "a.env", content }], { allow: [{ ruleId: "SEC001", fingerprint }] });
225
+ expect(findings).toEqual([]);
226
+ });
227
+
228
+ test("a fingerprint-only allow entry suppresses regardless of file", () => {
229
+ const fingerprint = fingerprintSecret(secret);
230
+ expect(scanForSecrets([{ path: "a.env", content }], { allow: [{ fingerprint }] })).toEqual([]);
231
+ });
232
+
233
+ test("a mismatched fingerprint does not suppress", () => {
234
+ const findings = scanForSecrets([{ path: "a.env", content }], { allow: [{ ruleId: "SEC001", fingerprint: "0000000000000000" }] });
235
+ expect(findings.map((f) => f.checkId)).toContain("SEC001");
236
+ });
237
+
238
+ test("`file` alone (no ruleId or fingerprint) never blanket-allows a file", () => {
239
+ const findings = scanForSecrets([{ path: "a.env", content }], { allow: [{ file: "a.env" }] });
240
+ expect(findings.map((f) => f.checkId)).toContain("SEC001");
241
+ });
242
+
243
+ test("file is a discriminator when combined with ruleId: a different file is unaffected", () => {
244
+ const fingerprint = fingerprintSecret(secret);
245
+ const findings = scanForSecrets(
246
+ [
247
+ { path: "a.env", content },
248
+ { path: "b.env", content },
249
+ ],
250
+ { allow: [{ ruleId: "SEC001", fingerprint, file: "a.env" }] },
251
+ );
252
+ expect(findings.map((f) => f.file)).toEqual(["b.env"]);
253
+ });
254
+ });
255
+
256
+ describe("parseSecretsConfig", () => {
257
+ test("parses a { secrets: {...} } wrapped config", () => {
258
+ const opts = parseSecretsConfig(
259
+ JSON.stringify({ secrets: { entropyThreshold: 4.5, entropyMinLength: 30, allow: [{ ruleId: "SEC010", file: "fixtures/x.env" }] } }),
260
+ );
261
+ expect(opts.entropyThreshold).toBe(4.5);
262
+ expect(opts.entropyMinLength).toBe(30);
263
+ expect(opts.allow).toEqual([{ ruleId: "SEC010", file: "fixtures/x.env", fingerprint: undefined }]);
264
+ });
265
+
266
+ test("parses a flat config with the same shape", () => {
267
+ const opts = parseSecretsConfig(JSON.stringify({ entropyThreshold: 3.5 }));
268
+ expect(opts.entropyThreshold).toBe(3.5);
269
+ });
270
+
271
+ test("is tolerant of malformed or empty content", () => {
272
+ expect(parseSecretsConfig("not json")).toEqual({});
273
+ expect(parseSecretsConfig("")).toEqual({});
274
+ expect(parseSecretsConfig("null")).toEqual({});
275
+ expect(parseSecretsConfig("[]")).toEqual({});
276
+ });
277
+
278
+ test("ignores non-object allow entries and non-string fields", () => {
279
+ const opts = parseSecretsConfig(JSON.stringify({ allow: [null, 42, "x", { ruleId: 7, file: "ok.env" }] }));
280
+ expect(opts.allow).toEqual([{ ruleId: undefined, file: "ok.env", fingerprint: undefined }]);
281
+ });
282
+ });
283
+
284
+ describe("scanForSecrets — file handling", () => {
285
+ test("empty content produces no findings", () => {
286
+ expect(scanForSecrets([{ path: "empty.txt", content: "" }])).toEqual([]);
287
+ });
288
+
289
+ test("multiple files are scanned independently, each finding keeps its own file", () => {
290
+ const secret = FAKE_AWS_KEY;
291
+ const findings = scanForSecrets([
292
+ { path: "one/.env", content: `A=${secret}\n` },
293
+ { path: "two/.env", content: "A=just-a-normal-value\n" },
294
+ ]);
295
+ expect(findings.map((f) => f.file)).toEqual(["one/.env"]);
296
+ });
297
+
298
+ test("every finding is merge-worthy-shaped: guidance fixKind, security-relevant severity", () => {
299
+ const findings = scanForSecrets([{ path: "a.env", content: `${FAKE_AWS_KEY}\n` }]);
300
+ expect(findings[0].severity).toBe("error");
301
+ expect(findings[0].lexicon).toBe("secrets");
302
+ });
303
+ });