@intentius/chant 0.46.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.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- 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 +3 -3
- 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/commands/lint.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 +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.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/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.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/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.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/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- 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/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.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/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.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 +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.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 +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- 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/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/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.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/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.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/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- 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/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- 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 +140 -3
- 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 +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -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/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.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 +49 -2
- 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/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- 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 +13 -1
- 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.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- 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/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- 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/auto-release.ts +6 -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/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- 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/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- 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/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- 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/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/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- 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/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -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/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- 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 +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.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 +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -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 +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/audit/catalog.ts
CHANGED
|
@@ -26,10 +26,13 @@ export type FixKind = "deterministic" | "guidance";
|
|
|
26
26
|
* What kind of finding this is — orthogonal to `tier` (fix confidence). Lets the
|
|
27
27
|
* report say "N security, M best-practice, K correctness" instead of branding
|
|
28
28
|
* everything "security." `security` = exposure/vuln/supply-chain; `correctness`
|
|
29
|
-
* = a structural bug (broken reference, invalid schema, never-runs);
|
|
30
|
-
*
|
|
29
|
+
* = a structural bug (broken reference, invalid schema, never-runs); `efficiency`
|
|
30
|
+
* (#444) = waste — redundant work, an oversized execution environment,
|
|
31
|
+
* unintended fan-out, unbounded retention — never a safety/correctness issue,
|
|
32
|
+
* so it is excluded from the security and correctness tallies; everything else
|
|
33
|
+
* is `best-practice` (hygiene/style/deprecation/reliability).
|
|
31
34
|
*/
|
|
32
|
-
export type Category = "security" | "correctness" | "best-practice";
|
|
35
|
+
export type Category = "security" | "correctness" | "best-practice" | "efficiency";
|
|
33
36
|
|
|
34
37
|
export interface Authority {
|
|
35
38
|
name: string;
|
|
@@ -96,6 +99,34 @@ export const K8S_SECRETS: Authority = {
|
|
|
96
99
|
name: "Kubernetes — Good practices for Secrets",
|
|
97
100
|
url: "https://kubernetes.io/docs/concepts/security/secrets-good-practices/",
|
|
98
101
|
};
|
|
102
|
+
export const GH_SECRET_SCANNING: Authority = {
|
|
103
|
+
name: "GitHub — About secret scanning",
|
|
104
|
+
url: "https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning",
|
|
105
|
+
};
|
|
106
|
+
export const SCORECARD_VULN: Authority = {
|
|
107
|
+
name: "OSSF Scorecard — Vulnerabilities",
|
|
108
|
+
url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities",
|
|
109
|
+
};
|
|
110
|
+
export const CF_WORKERS_DEV: Authority = {
|
|
111
|
+
name: "Cloudflare Workers — workers.dev",
|
|
112
|
+
url: "https://developers.cloudflare.com/workers/configuration/routing/workers-dev/",
|
|
113
|
+
};
|
|
114
|
+
export const CF_SECRETS: Authority = {
|
|
115
|
+
name: "Cloudflare Workers — Secrets",
|
|
116
|
+
url: "https://developers.cloudflare.com/workers/configuration/secrets/",
|
|
117
|
+
};
|
|
118
|
+
export const CF_ROUTES: Authority = {
|
|
119
|
+
name: "Cloudflare Workers — Routes",
|
|
120
|
+
url: "https://developers.cloudflare.com/workers/configuration/routing/routes/",
|
|
121
|
+
};
|
|
122
|
+
export const CF_ENVIRONMENTS: Authority = {
|
|
123
|
+
name: "Cloudflare Workers — Wrangler environments",
|
|
124
|
+
url: "https://developers.cloudflare.com/workers/wrangler/configuration/#environments",
|
|
125
|
+
};
|
|
126
|
+
export const CF_STATIC_ASSETS: Authority = {
|
|
127
|
+
name: "Cloudflare Workers — Static assets",
|
|
128
|
+
url: "https://developers.cloudflare.com/workers/static-assets/",
|
|
129
|
+
};
|
|
99
130
|
|
|
100
131
|
function meta(
|
|
101
132
|
id: string,
|
|
@@ -127,6 +158,22 @@ const G = "guidance" as const;
|
|
|
127
158
|
export const RULE_CATEGORY: Record<string, Category> = {
|
|
128
159
|
COR020: "correctness",
|
|
129
160
|
EXT001: "correctness",
|
|
161
|
+
SEC001: "security",
|
|
162
|
+
SEC002: "security",
|
|
163
|
+
SEC003: "security",
|
|
164
|
+
SEC004: "security",
|
|
165
|
+
SEC005: "security",
|
|
166
|
+
SEC006: "security",
|
|
167
|
+
SEC007: "security",
|
|
168
|
+
SEC008: "security",
|
|
169
|
+
SEC009: "security",
|
|
170
|
+
SEC010: "security",
|
|
171
|
+
WRG001: "security",
|
|
172
|
+
WRG002: "security",
|
|
173
|
+
WRG003: "best-practice",
|
|
174
|
+
WRG004: "security",
|
|
175
|
+
WRG005: "security",
|
|
176
|
+
WRG006: "security",
|
|
130
177
|
};
|
|
131
178
|
|
|
132
179
|
/**
|
|
@@ -139,6 +186,31 @@ export const RULE_CATEGORY: Record<string, Category> = {
|
|
|
139
186
|
export const RULE_CATALOG: Record<string, RuleMeta> = {
|
|
140
187
|
COR020: meta("COR020", M, G, "Circular resource dependency", "Break the dependency cycle between resources."),
|
|
141
188
|
EXT001: meta("EXT001", M, G, "Extension constraint violation", "Fix the cross-property constraint flagged by the cfn-lint extension schema."),
|
|
189
|
+
|
|
190
|
+
// Secrets & credentials (#443) — lexicon-independent: `secrets.ts` scans the
|
|
191
|
+
// raw text of every candidate file, so these ids apply regardless of which
|
|
192
|
+
// (if any) audit lexicons are installed. `fixKind` is `guidance`: removing
|
|
193
|
+
// a hardcoded credential and rotating it needs a human, never an auto-fix.
|
|
194
|
+
SEC001: meta("SEC001", M, G, "AWS access key ID found", "Remove the key from source, rotate it in IAM, and load it from a secret store or environment variable instead.", [GH_SECRET_SCANNING]),
|
|
195
|
+
SEC002: meta("SEC002", M, G, "AWS secret access key found", "Remove the key from source, rotate it in IAM, and load it from a secret store or environment variable instead.", [GH_SECRET_SCANNING]),
|
|
196
|
+
SEC003: meta("SEC003", M, G, "GitHub token found", "Remove the token from source and revoke it at github.com/settings/tokens; use a GitHub Actions secret instead.", [GH_SECRET_SCANNING]),
|
|
197
|
+
SEC004: meta("SEC004", M, G, "Slack token found", "Remove the token from source and revoke it in the Slack app's OAuth settings.", [GH_SECRET_SCANNING]),
|
|
198
|
+
SEC005: meta("SEC005", M, G, "Google API key found", "Remove the key from source and regenerate it in the Google Cloud Console credentials page.", [GH_SECRET_SCANNING]),
|
|
199
|
+
SEC006: meta("SEC006", M, G, "Stripe live secret key found", "Remove the key from source and roll it in the Stripe dashboard immediately — this is a live-mode key.", [GH_SECRET_SCANNING]),
|
|
200
|
+
SEC007: meta("SEC007", M, G, "Private key block found", "Remove the private key from source, rotate the keypair, and load the key from a secret store instead.", [GH_SECRET_SCANNING]),
|
|
201
|
+
SEC008: meta("SEC008", M, G, "Bearer/authorization token found", "Remove the token from source; if it's long-lived, revoke and reissue it via the issuing service.", [GH_SECRET_SCANNING]),
|
|
202
|
+
SEC009: meta("SEC009", M, G, "Credentials embedded in a connection string", "Move the username/password out of the URI into a secret store, and rotate the credential.", [GH_SECRET_SCANNING]),
|
|
203
|
+
SEC010: meta("SEC010", M, G, "High-entropy string — possible secret", "Confirm whether this is a live credential; if so, remove it from source and rotate it. If it's a false positive, suppress with a `chant-audit-ignore` comment or an allowlist entry.", [GH_SECRET_SCANNING]),
|
|
204
|
+
|
|
205
|
+
// Wrangler config audit (#446) — lexicon-independent, same shape as the SEC
|
|
206
|
+
// family above: `wrangler.ts` scans every `wrangler.toml` it finds
|
|
207
|
+
// regardless of which (if any) audit lexicons are installed.
|
|
208
|
+
WRG001: meta("WRG001", M, G, "Production environment exposed on *.workers.dev", "Remove workers_dev (or set it to false) for this environment and rely on its custom domain/route instead of the shared public subdomain.", [CF_WORKERS_DEV]),
|
|
209
|
+
WRG002: meta("WRG002", M, G, "Credential-shaped key stored in [vars]", "Move the value out of [vars] and into `wrangler secret put <name>` so it isn't committed to source or visible in `wrangler dev`/the dashboard.", [CF_SECRETS]),
|
|
210
|
+
WRG003: meta("WRG003", R, G, "Observability explicitly disabled", "Set observability.enabled = true (or remove the override) so Workers Logs are recorded for this deployment."),
|
|
211
|
+
WRG004: meta("WRG004", M, G, "Unscoped wildcard route", "Scope the route pattern to the intended zone (e.g. \"example.com/*\") instead of a bare \"*\" or \"*/*\" that matches every zone on the account.", [CF_ROUTES]),
|
|
212
|
+
WRG005: meta("WRG005", M, G, "Non-production environment shares a data store with production", "Give the non-production environment its own KV namespace/R2 bucket/D1 database id instead of reusing production's.", [CF_ENVIRONMENTS]),
|
|
213
|
+
WRG006: meta("WRG006", M, G, "Static assets served from the project root", "Point [site].bucket / [assets].directory at a dedicated public output folder, not the project root, so non-public files (config, source maps, .git) aren't served.", [CF_STATIC_ASSETS]),
|
|
142
214
|
};
|
|
143
215
|
|
|
144
216
|
/**
|
package/src/audit/core.test.ts
CHANGED
|
@@ -187,4 +187,61 @@ describe("auditFiles", () => {
|
|
|
187
187
|
});
|
|
188
188
|
expect(findings.map((f) => f.checkId)).toEqual(["OK001"]);
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
test("parse-to-graph: an injected entities parser lets entity-reading checks fire per file (#1567)", async () => {
|
|
192
|
+
const entityCheck: PostSynthCheck = {
|
|
193
|
+
id: "ENT001",
|
|
194
|
+
description: "reads ctx.entities, not ctx.outputs",
|
|
195
|
+
check: (ctx) =>
|
|
196
|
+
[...ctx.entities.keys()].map((name) => ({ checkId: "ENT001", severity: "warning" as const, message: `saw ${name}`, entity: name })),
|
|
197
|
+
};
|
|
198
|
+
const findings = await auditFiles(
|
|
199
|
+
[
|
|
200
|
+
{ path: "a.yaml", content: "one", lexicon: "custom" },
|
|
201
|
+
{ path: "b.yaml", content: "two", lexicon: "custom" },
|
|
202
|
+
],
|
|
203
|
+
{
|
|
204
|
+
checksProvider: async () => [entityCheck],
|
|
205
|
+
entitiesProvider: async () => (content) => new Map([[content, { entityType: "T::X" } as never]]),
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
expect(findings.map((f) => `${f.file}:${f.entity}`).sort()).toEqual(["a.yaml:one", "b.yaml:two"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("parse-to-graph merge: the same entity name in two files keeps both — a duplicate check sees the collision cross-file", async () => {
|
|
212
|
+
const dupCheck: PostSynthCheck = {
|
|
213
|
+
id: "DUP001",
|
|
214
|
+
description: "fires when more than one entity is present",
|
|
215
|
+
check: (ctx) => (ctx.entities.size > 1 ? [{ checkId: "DUP001", severity: "error" as const, message: "name declared twice" }] : []),
|
|
216
|
+
};
|
|
217
|
+
const findings = await auditFiles(
|
|
218
|
+
[
|
|
219
|
+
{ path: "a.yaml", content: "x", lexicon: "custom" },
|
|
220
|
+
{ path: "b.yaml", content: "y", lexicon: "custom" },
|
|
221
|
+
],
|
|
222
|
+
{
|
|
223
|
+
checksProvider: async () => [dupCheck],
|
|
224
|
+
// both files declare the entity name "dev" — the merged pass must keep both
|
|
225
|
+
entitiesProvider: async () => () => new Map([["dev", { entityType: "T::X" } as never]]),
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
const dup = findings.find((f) => f.checkId === "DUP001");
|
|
229
|
+
expect(dup).toBeDefined();
|
|
230
|
+
expect(dup!.file).toBe(CROSS_FILE);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("an entities parser that throws contributes no entities and does not abort the audit", async () => {
|
|
234
|
+
const entityCheck: PostSynthCheck = {
|
|
235
|
+
id: "ENT002",
|
|
236
|
+
description: "counts entities",
|
|
237
|
+
check: (ctx) => (ctx.entities.size > 0 ? [{ checkId: "ENT002", severity: "warning" as const, message: "saw entities" }] : []),
|
|
238
|
+
};
|
|
239
|
+
const findings = await auditFiles([{ path: "a.yaml", content: "x", lexicon: "custom" }], {
|
|
240
|
+
checksProvider: async () => [entityCheck],
|
|
241
|
+
entitiesProvider: async () => () => {
|
|
242
|
+
throw new Error("unparseable");
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
expect(findings).toEqual([]);
|
|
246
|
+
});
|
|
190
247
|
});
|
package/src/audit/core.ts
CHANGED
|
Binary file
|
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
* service content-detect all lexicons on Workers.
|
|
10
10
|
*/
|
|
11
11
|
const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
12
|
-
const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm"];
|
|
12
|
+
const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"];
|
|
13
13
|
|
|
14
14
|
describe("detectTemplate modules are edge-bundle clean", () => {
|
|
15
15
|
test("importing every lexicon's /detect pulls in no typescript or plugin.ts", async () => {
|
|
@@ -42,6 +42,29 @@ describe("discoverByDetection (unified, detectTemplate-driven)", () => {
|
|
|
42
42
|
expect(found.some((i) => i.lexicon === "k8s")).toBe(false);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test("fountain wins over k8s in a mixed repo — each manifest classifies to its own lexicon (#1566)", async () => {
|
|
46
|
+
const plugins = await loadAuditPlugins();
|
|
47
|
+
const found = discoverByDetection(fixture("audit-fountain"), plugins);
|
|
48
|
+
expect(targets(found)).toEqual(["fountain:agents/fleet.yaml", "k8s:k8s/deploy.yaml"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a multi-document fountain file classifies as one fountain input (no document dropped)", async () => {
|
|
52
|
+
const plugins = await loadAuditPlugins();
|
|
53
|
+
const found = discoverByDetection(fixture("audit-fountain"), plugins);
|
|
54
|
+
const fountain = found.find((i) => i.lexicon === "fountain")!;
|
|
55
|
+
// All three documents ride along in the input's content.
|
|
56
|
+
expect(fountain.content.match(/apiVersion: fountain\.dev\/v1/g)).toHaveLength(3);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("without the fountain plugin, fountain YAML falls to k8s (not-installed convention)", () => {
|
|
60
|
+
const files = [{ path: "fleet.yaml", content: "apiVersion: fountain.dev/v1\nkind: Environment\nmetadata:\n name: dev\n" }];
|
|
61
|
+
const k8s = { name: "k8s", detectTemplate: (d: unknown) => !!d && typeof d === "object" && "apiVersion" in (d as object) && "kind" in (d as object) };
|
|
62
|
+
expect(classifyFiles(files, [k8s]).map((i) => i.lexicon)).toEqual(["k8s"]);
|
|
63
|
+
// with a fountain plugin present, fountain claims it first
|
|
64
|
+
const fountain = { name: "fountain", detectTemplate: (d: unknown) => (d as { apiVersion?: unknown })?.apiVersion === "fountain.dev/v1" };
|
|
65
|
+
expect(classifyFiles(files, [k8s, fountain]).map((i) => i.lexicon)).toEqual(["fountain"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
45
68
|
test("detects a Helm chart as a bundle, not loose manifests", async () => {
|
|
46
69
|
const plugins = await loadAuditPlugins();
|
|
47
70
|
const found = discoverByDetection(fixture("audit-helm"), plugins);
|
|
@@ -80,6 +103,7 @@ describe("lexicon hints for unclaimed files (#1623)", () => {
|
|
|
80
103
|
expect(hintLexiconForFile("charts/x/Chart.yaml", "name: x\n")).toBe("helm");
|
|
81
104
|
expect(hintLexiconForFile("infra/main.tf", "resource {}\n")).toBe("terraform");
|
|
82
105
|
expect(hintLexiconForFile("k8s/deploy.yaml", "apiVersion: apps/v1\nkind: Deployment\n")).toBe("k8s");
|
|
106
|
+
expect(hintLexiconForFile("agents/fleet.yaml", "apiVersion: fountain.dev/v1\nkind: Environment\n")).toBe("fountain");
|
|
83
107
|
expect(hintLexiconForFile("gcp/bucket.yaml", "apiVersion: storage.cnrm.cloud.google.com/v1beta1\nkind: StorageBucket\n")).toBe("gcp");
|
|
84
108
|
expect(hintLexiconForFile("cfn/stack.yaml", "AWSTemplateFormatVersion: '2010-09-09'\nResources: {}\n")).toBe("aws");
|
|
85
109
|
expect(hintLexiconForFile("cfn/stack.json", '{"Resources":{"B":{"Type":"AWS::S3::Bucket"}}}')).toBe("aws");
|
package/src/audit/discover.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* - Helm charts are a directory BUNDLE keyed by `Chart.yaml`; the helm checks
|
|
23
23
|
* read `output.files`, so the whole chart is one AuditInput.
|
|
24
24
|
* - k8s `detectTemplate` matches any `apiVersion`+`kind`, including GCP Config
|
|
25
|
-
* Connector (`cnrm.cloud.google.com`) resources
|
|
25
|
+
* Connector (`cnrm.cloud.google.com`) resources and fountain
|
|
26
|
+
* (`fountain.dev/v1`) manifests, so gcp and fountain must be tried first.
|
|
26
27
|
* - aws/azure checks `JSON.parse` their input, so YAML/templated content is
|
|
27
28
|
* normalized to a JSON string here.
|
|
28
29
|
*/
|
|
@@ -34,7 +35,7 @@ import type { LexiconPlugin } from "../lexicon";
|
|
|
34
35
|
import type { AuditInput, AuditLexicon } from "./core";
|
|
35
36
|
|
|
36
37
|
/** Lexicons the auditor knows how to detect and run checks for. */
|
|
37
|
-
export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm"] as const;
|
|
38
|
+
export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"] as const;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Load the plugins used for detection. Each is loaded in isolation (no
|
|
@@ -103,6 +104,27 @@ function isDockerfileName(name: string): boolean {
|
|
|
103
104
|
return name === "Dockerfile" || name.startsWith("Dockerfile.") || name.endsWith(".Dockerfile") || name.endsWith(".dockerfile");
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Filenames the secrets scan (#443) wants that no lexicon otherwise reads —
|
|
109
|
+
* dotenv files and key/cert material. Included in `isCandidatePath` so both
|
|
110
|
+
* the local walk and the remote fetch pick them up; `classifyFiles` doesn't
|
|
111
|
+
* claim them for any lexicon, they just ride along for `scanForSecrets`.
|
|
112
|
+
*/
|
|
113
|
+
function isSecretBearingName(name: string): boolean {
|
|
114
|
+
if (/^\.env(\..+)?$/i.test(name)) return true;
|
|
115
|
+
return /\.(pem|key|crt|cer|pfx|p12)$/i.test(name);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `wrangler.toml` (#446) — Cloudflare Workers' declarative deploy config.
|
|
120
|
+
* Native TOML, so it rides along the same way secret-bearing filenames do:
|
|
121
|
+
* `wrangler.ts` reads it directly (no lexicon plugin, no authoring surface),
|
|
122
|
+
* so it only needs to be a candidate path, not a `detectTemplate` entry.
|
|
123
|
+
*/
|
|
124
|
+
function isWranglerConfigName(name: string): boolean {
|
|
125
|
+
return name === "wrangler.toml";
|
|
126
|
+
}
|
|
127
|
+
|
|
106
128
|
/** Split a possibly multi-document YAML string into per-doc parsed objects. */
|
|
107
129
|
function parseDocs(content: string): Record<string, unknown>[] {
|
|
108
130
|
const docs: Record<string, unknown>[] = [];
|
|
@@ -170,6 +192,13 @@ const CONTENT_DETECTORS: ContentDetector[] = [
|
|
|
170
192
|
accepts: isYaml,
|
|
171
193
|
detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
|
|
172
194
|
},
|
|
195
|
+
{
|
|
196
|
+
// fountain (`apiVersion: fountain.dev/v1`) — must win over k8s, whose
|
|
197
|
+
// detectTemplate matches any apiVersion+kind document (#1566).
|
|
198
|
+
lexicon: "fountain",
|
|
199
|
+
accepts: isYaml,
|
|
200
|
+
detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
|
|
201
|
+
},
|
|
173
202
|
{
|
|
174
203
|
lexicon: "k8s",
|
|
175
204
|
accepts: isYaml,
|
|
@@ -223,8 +252,12 @@ export interface DetectPlugin {
|
|
|
223
252
|
detectTemplate?: (data: unknown) => boolean;
|
|
224
253
|
}
|
|
225
254
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Which CI lexicon a path belongs to (by location — content can't disambiguate
|
|
257
|
+
* github vs forgejo). Exported for the fetch module: known-path lexicons stay a
|
|
258
|
+
* direct path match even in search-first (large-repo) discovery (#520).
|
|
259
|
+
*/
|
|
260
|
+
export function ciLexiconForPath(path: string): AuditLexicon | undefined {
|
|
228
261
|
if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(path)) return "github";
|
|
229
262
|
if (/^\.forgejo\/workflows\/[^/]+\.ya?ml$/.test(path)) return "forgejo";
|
|
230
263
|
if (path === ".gitlab-ci.yml") return "gitlab";
|
|
@@ -240,6 +273,8 @@ export function isCandidatePath(path: string): boolean {
|
|
|
240
273
|
if (ciLexiconForPath(path)) return true;
|
|
241
274
|
if (isDockerfileName(name)) return true;
|
|
242
275
|
if (name === "Chart.yaml") return true;
|
|
276
|
+
if (isSecretBearingName(name)) return true;
|
|
277
|
+
if (isWranglerConfigName(name)) return true;
|
|
243
278
|
return /\.(ya?ml|json|template)$/i.test(name);
|
|
244
279
|
}
|
|
245
280
|
|
|
@@ -263,6 +298,7 @@ export function hintLexiconForFile(path: string, content: string): LexiconHint |
|
|
|
263
298
|
if (/\.tf$/i.test(name)) return "terraform";
|
|
264
299
|
const head = content.slice(0, 64 * 1024);
|
|
265
300
|
if (/cnrm\.cloud\.google\.com/.test(head)) return "gcp";
|
|
301
|
+
if (/fountain\.dev\/v1/.test(head)) return "fountain";
|
|
266
302
|
if (/AWSTemplateFormatVersion|["']?Type["']?\s*:\s*["']AWS::/.test(head)) return "aws";
|
|
267
303
|
if (/deploymentTemplate\.json/.test(head)) return "azure";
|
|
268
304
|
if (isYaml(name)) {
|
package/src/audit/fetch.test.ts
CHANGED
|
@@ -54,15 +54,28 @@ function gitTreeMock(files: Record<string, string>, sizes: Record<string, number
|
|
|
54
54
|
return { impl, calls };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** A full root-level tree page (>=100 entries) — the "large repo" signal `listTreeGitLab` probes for. */
|
|
58
|
+
const LARGE_ROOT_PAGE = Array.from({ length: 100 }, (_, i) => ({ path: `dir${i}`, type: "tree" }));
|
|
59
|
+
/** A small root-level tree page (<100 entries) — the "small repo" signal, keeps the walk. */
|
|
60
|
+
const SMALL_ROOT_PAGE: Array<{ path: string; type: string }> = [];
|
|
61
|
+
|
|
57
62
|
/**
|
|
58
63
|
* GitLab mock for authenticated search-based discovery. `searchMap` maps search
|
|
59
64
|
* term → array of blob paths the search API returns. `files` maps path → content.
|
|
60
|
-
*
|
|
61
|
-
* search path
|
|
65
|
+
* The root tree probe reports a large repo by default (`rootPage`
|
|
66
|
+
* overridable) so these mocks exercise the search path; pass `token` to
|
|
67
|
+
* `fetchRepoFiles` too, since search additionally requires authentication.
|
|
62
68
|
*/
|
|
63
|
-
function gitlabMock(searchMap: Record<string, string[]>, files: Record<string, string>) {
|
|
69
|
+
function gitlabMock(searchMap: Record<string, string[]>, files: Record<string, string>, rootPage: Array<{ path: string; type: string }> = LARGE_ROOT_PAGE) {
|
|
64
70
|
const impl = (async (url: string | URL | Request) => {
|
|
65
71
|
const u = String(url);
|
|
72
|
+
if (u.includes("/repository/tree")) {
|
|
73
|
+
const params = new URL(u).searchParams;
|
|
74
|
+
const path = params.get("path") ?? "";
|
|
75
|
+
const page = Number(params.get("page") ?? "1");
|
|
76
|
+
const entries = path === "" && page === 1 ? rootPage : [];
|
|
77
|
+
return new Response(JSON.stringify(entries), { status: 200 });
|
|
78
|
+
}
|
|
66
79
|
if (u.includes("/search")) {
|
|
67
80
|
const params = new URL(u).searchParams;
|
|
68
81
|
const term = params.get("search") ?? "";
|
|
@@ -112,6 +125,85 @@ function gitlabBfsMock(
|
|
|
112
125
|
return { impl };
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A GitHub-shaped mock for large-repo (search-first) discovery: `fillerCount`
|
|
130
|
+
* inflates the recursive tree past `LARGE_REPO_TREE_ENTRIES` so `listTree`
|
|
131
|
+
* switches strategies; `searchMap` maps a bare search term (no `repo:` qualifier)
|
|
132
|
+
* to the paths GitHub's code search returns for it, paginated 100/page.
|
|
133
|
+
*/
|
|
134
|
+
function githubCodeSearchMock(fillerCount: number, files: Record<string, string>, searchMap: Record<string, string[]>) {
|
|
135
|
+
const calls: Array<{ url: string }> = [];
|
|
136
|
+
const impl = (async (url: string | URL | Request) => {
|
|
137
|
+
const u = String(url);
|
|
138
|
+
calls.push({ url: u });
|
|
139
|
+
if (u.includes("/git/trees/")) {
|
|
140
|
+
const filler = Array.from({ length: fillerCount }, (_, i) => ({ path: `filler/file${i}.md`, type: "blob", size: 10 }));
|
|
141
|
+
const real = Object.keys(files).map((path) => ({ path, type: "blob", size: files[path].length }));
|
|
142
|
+
return new Response(JSON.stringify({ tree: [...filler, ...real] }), { status: 200 });
|
|
143
|
+
}
|
|
144
|
+
if (u.includes("/search/code")) {
|
|
145
|
+
const params = new URL(u).searchParams;
|
|
146
|
+
const term = (params.get("q") ?? "").replace(/\s*repo:.*/, "");
|
|
147
|
+
const page = Number(params.get("page") ?? "1");
|
|
148
|
+
const all = searchMap[term] ?? [];
|
|
149
|
+
const start = (page - 1) * 100;
|
|
150
|
+
const items = all.slice(start, start + 100).map((path) => ({ path }));
|
|
151
|
+
return new Response(JSON.stringify({ items, total_count: all.length }), { status: 200 });
|
|
152
|
+
}
|
|
153
|
+
const rawm = u.match(/raw\.githubusercontent\.com\/[^/]+\/[^/]+\/[^/]+\/(.+)$/);
|
|
154
|
+
if (rawm) {
|
|
155
|
+
const path = decodeURIComponent(rawm[1]);
|
|
156
|
+
if (files[path] === undefined) return new Response("not found", { status: 404 });
|
|
157
|
+
return new Response(files[path], { status: 200 });
|
|
158
|
+
}
|
|
159
|
+
const cm = u.match(/\/contents\/(.+?)\?/);
|
|
160
|
+
if (cm) {
|
|
161
|
+
const path = decodeURIComponent(cm[1]);
|
|
162
|
+
if (files[path] === undefined) return new Response("not found", { status: 404 });
|
|
163
|
+
return new Response(JSON.stringify({ path, type: "file", content: b64(files[path]), encoding: "base64" }), { status: 200 });
|
|
164
|
+
}
|
|
165
|
+
if (/\/repos\/[^/]+\/[^/]+(\?|$)/.test(u)) return new Response(JSON.stringify({ default_branch: "main" }), { status: 200 });
|
|
166
|
+
return new Response("not found", { status: 404 });
|
|
167
|
+
}) as unknown as typeof fetch;
|
|
168
|
+
return { impl, calls };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A Forgejo/Gitea-shaped mock for large-repo discovery. `searchMap` maps a bare
|
|
173
|
+
* search term to the paths its (best-effort, `{ok, data}`-enveloped) search
|
|
174
|
+
* endpoint returns, paginated 100/page.
|
|
175
|
+
*/
|
|
176
|
+
function forgejoCodeSearchMock(fillerCount: number, files: Record<string, string>, searchMap: Record<string, string[]>) {
|
|
177
|
+
const calls: Array<{ url: string }> = [];
|
|
178
|
+
const impl = (async (url: string | URL | Request) => {
|
|
179
|
+
const u = String(url);
|
|
180
|
+
calls.push({ url: u });
|
|
181
|
+
if (u.includes("/git/trees/")) {
|
|
182
|
+
const filler = Array.from({ length: fillerCount }, (_, i) => ({ path: `filler/file${i}.md`, type: "blob", size: 10 }));
|
|
183
|
+
const real = Object.keys(files).map((path) => ({ path, type: "blob", size: files[path].length }));
|
|
184
|
+
return new Response(JSON.stringify({ tree: [...filler, ...real] }), { status: 200 });
|
|
185
|
+
}
|
|
186
|
+
if (u.includes("/search")) {
|
|
187
|
+
const params = new URL(u).searchParams;
|
|
188
|
+
const term = params.get("q") ?? "";
|
|
189
|
+
const page = Number(params.get("page") ?? "1");
|
|
190
|
+
const all = searchMap[term] ?? [];
|
|
191
|
+
const start = (page - 1) * 100;
|
|
192
|
+
const data = all.slice(start, start + 100).map((path) => ({ path }));
|
|
193
|
+
return new Response(JSON.stringify({ ok: true, data }), { status: 200 });
|
|
194
|
+
}
|
|
195
|
+
const cm = u.match(/\/contents\/(.+?)\?/);
|
|
196
|
+
if (cm) {
|
|
197
|
+
const path = decodeURIComponent(cm[1]);
|
|
198
|
+
if (files[path] === undefined) return new Response("not found", { status: 404 });
|
|
199
|
+
return new Response(JSON.stringify({ path, type: "file", content: b64(files[path]), encoding: "base64" }), { status: 200 });
|
|
200
|
+
}
|
|
201
|
+
if (/\/repos\/[^/]+\/[^/]+(\?|$)/.test(u)) return new Response(JSON.stringify({ default_branch: "main" }), { status: 200 });
|
|
202
|
+
return new Response("not found", { status: 404 });
|
|
203
|
+
}) as unknown as typeof fetch;
|
|
204
|
+
return { impl, calls };
|
|
205
|
+
}
|
|
206
|
+
|
|
115
207
|
describe("parseRepoUrl", () => {
|
|
116
208
|
test("rejects non-https", () => {
|
|
117
209
|
expect(() => parseRepoUrl("http://github.com/o/r")).toThrow(FetchError);
|
|
@@ -285,12 +377,133 @@ describe("fetchRepoFiles", () => {
|
|
|
285
377
|
expect(files.map((f) => f.path)).toContain(".gitlab-ci.yml");
|
|
286
378
|
});
|
|
287
379
|
|
|
380
|
+
// ── Strategy selection: small repo → walk, large repo → search (#520) ────
|
|
381
|
+
|
|
382
|
+
test("gitlab: a small repo uses the walk even when a token is present (no search call)", async () => {
|
|
383
|
+
// The root page has one blob and no filler dirs — well under the large-repo
|
|
384
|
+
// threshold — so the walk is used regardless of the token, and the search
|
|
385
|
+
// API is never called.
|
|
386
|
+
const rootPage = [{ path: ".gitlab-ci.yml", type: "blob" }];
|
|
387
|
+
const { impl } = gitlabMock({ "stages:": [".gitlab-ci.yml"] }, { ".gitlab-ci.yml": "stages:\n - build\n" }, rootPage);
|
|
388
|
+
const calls: string[] = [];
|
|
389
|
+
const spy = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
390
|
+
calls.push(String(url));
|
|
391
|
+
return impl(url, init);
|
|
392
|
+
}) as unknown as typeof fetch;
|
|
393
|
+
const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: spy, token: "tok" });
|
|
394
|
+
expect(files.map((f) => f.path)).toEqual([".gitlab-ci.yml"]);
|
|
395
|
+
expect(calls.some((c) => c.includes("/search"))).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("gitlab: a large repo without a token falls back to the BFS walk (search needs auth)", async () => {
|
|
399
|
+
const dirEntries = {
|
|
400
|
+
"": [{ path: ".gitlab-ci.yml", type: "blob" as const }, ...Array.from({ length: 99 }, (_, i) => ({ path: `dir${i}`, type: "tree" as const }))],
|
|
401
|
+
};
|
|
402
|
+
const { impl } = gitlabBfsMock(dirEntries, { ".gitlab-ci.yml": "stages:\n - build\n" });
|
|
403
|
+
const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl }); // no token
|
|
404
|
+
expect(files.map((f) => f.path)).toContain(".gitlab-ci.yml");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("gitlab: search failure on a large repo falls back to the BFS walk (#520)", async () => {
|
|
408
|
+
const dirEntries = {
|
|
409
|
+
"": [{ path: ".gitlab-ci.yml", type: "blob" as const }, ...Array.from({ length: 99 }, (_, i) => ({ path: `dir${i}`, type: "tree" as const }))],
|
|
410
|
+
};
|
|
411
|
+
const { impl: bfsImpl } = gitlabBfsMock(dirEntries, { ".gitlab-ci.yml": "stages:\n - build\n" });
|
|
412
|
+
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
413
|
+
if (String(url).includes("/search")) return new Response("rate limited", { status: 429 });
|
|
414
|
+
return bfsImpl(url, init);
|
|
415
|
+
}) as unknown as typeof fetch;
|
|
416
|
+
const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl, token: "tok" });
|
|
417
|
+
expect(files.map((f) => f.path)).toContain(".gitlab-ci.yml");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("github: a small repo never calls the code search API", async () => {
|
|
421
|
+
const { impl, calls } = gitTreeMock({ ".github/workflows/ci.yml": CI_YAML, "k8s/deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\n" });
|
|
422
|
+
await fetchRepoFiles("https://github.com/acme/widgets", { fetchImpl: impl });
|
|
423
|
+
expect(calls.some((c) => c.url.includes("/search/code"))).toBe(false);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("github: a large repo switches to per-lexicon search; known-path CI bypasses search entirely", async () => {
|
|
427
|
+
const files = { ".github/workflows/ci.yml": CI_YAML, "infra/eks.yaml": "apiVersion: v1\nkind: ConfigMap\n" };
|
|
428
|
+
const { impl, calls } = githubCodeSearchMock(600, files, { apiVersion: ["infra/eks.yaml"] });
|
|
429
|
+
const found = await fetchRepoFiles("https://github.com/acme/monorepo", { fetchImpl: impl, maxFiles: 100 });
|
|
430
|
+
const paths = found.map((f) => f.path);
|
|
431
|
+
expect(paths).toContain(".github/workflows/ci.yml");
|
|
432
|
+
expect(paths).toContain("infra/eks.yaml");
|
|
433
|
+
// The workflow came straight from the tree already in hand — no search
|
|
434
|
+
// query was needed (or issued) to find it.
|
|
435
|
+
expect(calls.some((c) => c.url.includes("/search/code") && c.url.includes("workflows"))).toBe(false);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("github: search results paginate past 100 until a short page", async () => {
|
|
439
|
+
const manyPaths = Array.from({ length: 150 }, (_, i) => `k8s/deploy${i}.yaml`);
|
|
440
|
+
const files: Record<string, string> = {};
|
|
441
|
+
for (const p of manyPaths) files[p] = "apiVersion: apps/v1\nkind: Deployment\n";
|
|
442
|
+
const { impl, calls } = githubCodeSearchMock(600, files, { apiVersion: manyPaths });
|
|
443
|
+
const found = await fetchRepoFiles("https://github.com/acme/monorepo", { fetchImpl: impl, maxFiles: 1000 });
|
|
444
|
+
expect(found.length).toBe(150);
|
|
445
|
+
const searchCalls = calls.filter((c) => c.url.includes("/search/code") && c.url.includes("q=apiVersion"));
|
|
446
|
+
expect(searchCalls.some((c) => c.url.includes("page=1"))).toBe(true);
|
|
447
|
+
expect(searchCalls.some((c) => c.url.includes("page=2"))).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("github: search failure (rate limit) on a large repo falls back to the full tree", async () => {
|
|
451
|
+
const files = { ".github/workflows/ci.yml": CI_YAML, "infra/eks.yaml": "apiVersion: v1\nkind: ConfigMap\n" };
|
|
452
|
+
const { impl: base } = githubCodeSearchMock(600, files, {});
|
|
453
|
+
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
454
|
+
if (String(url).includes("/search/code")) return new Response("rate limited", { status: 403 });
|
|
455
|
+
return base(url, init);
|
|
456
|
+
}) as unknown as typeof fetch;
|
|
457
|
+
const found = await fetchRepoFiles("https://github.com/acme/monorepo", { fetchImpl: impl, maxFiles: 1000 });
|
|
458
|
+
const paths = found.map((f) => f.path);
|
|
459
|
+
expect(paths).toContain(".github/workflows/ci.yml");
|
|
460
|
+
expect(paths).toContain("infra/eks.yaml");
|
|
461
|
+
});
|
|
462
|
+
|
|
288
463
|
test("forgejo (codeberg): gitea tree + contents", async () => {
|
|
289
464
|
const { impl } = gitTreeMock({ ".forgejo/workflows/ci.yml": CI_YAML });
|
|
290
465
|
const files = await fetchRepoFiles("https://codeberg.org/acme/widgets", { fetchImpl: impl });
|
|
291
466
|
expect(files.map((f) => f.path)).toEqual([".forgejo/workflows/ci.yml"]);
|
|
292
467
|
});
|
|
293
468
|
|
|
469
|
+
test("forgejo: a large repo switches to search; known-path CI bypasses search entirely", async () => {
|
|
470
|
+
const files = { ".forgejo/workflows/ci.yml": CI_YAML, "charts/app/Chart.yaml": "apiVersion: v2\nname: app\nversion: 1.0.0\n" };
|
|
471
|
+
const { impl, calls } = forgejoCodeSearchMock(600, files, { "apiVersion: v2": ["charts/app/Chart.yaml"] });
|
|
472
|
+
const found = await fetchRepoFiles("https://codeberg.org/acme/monorepo", { fetchImpl: impl, maxFiles: 100 });
|
|
473
|
+
const paths = found.map((f) => f.path);
|
|
474
|
+
expect(paths).toContain(".forgejo/workflows/ci.yml");
|
|
475
|
+
expect(paths).toContain("charts/app/Chart.yaml");
|
|
476
|
+
expect(calls.some((c) => c.url.includes("/search") && c.url.includes("workflows"))).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("forgejo: search paginates past 100 until a short page", async () => {
|
|
480
|
+
const manyPaths = Array.from({ length: 120 }, (_, i) => `k8s/deploy${i}.yaml`);
|
|
481
|
+
const files: Record<string, string> = {};
|
|
482
|
+
for (const p of manyPaths) files[p] = "apiVersion: apps/v1\nkind: Deployment\n";
|
|
483
|
+
const { impl, calls } = forgejoCodeSearchMock(600, files, { apiVersion: manyPaths });
|
|
484
|
+
const found = await fetchRepoFiles("https://codeberg.org/acme/monorepo", { fetchImpl: impl, maxFiles: 1000 });
|
|
485
|
+
expect(found.length).toBe(120);
|
|
486
|
+
const searchCalls = calls.filter((c) => c.url.includes("/search") && c.url.includes("q=apiVersion"));
|
|
487
|
+
expect(searchCalls.some((c) => c.url.includes("page=1"))).toBe(true);
|
|
488
|
+
expect(searchCalls.some((c) => c.url.includes("page=2"))).toBe(true);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("forgejo: an instance with no code search (404) falls back to the full tree", async () => {
|
|
492
|
+
// Forgejo's code-search availability/shape is undocumented (#520) — an
|
|
493
|
+
// instance without a code indexer, or with a different API shape, must
|
|
494
|
+
// still produce a full audit via the fallback, not an empty/failed one.
|
|
495
|
+
const files = { ".forgejo/workflows/ci.yml": CI_YAML, "charts/app/Chart.yaml": "apiVersion: v2\nname: app\nversion: 1.0.0\n" };
|
|
496
|
+
const { impl: base } = forgejoCodeSearchMock(600, files, {});
|
|
497
|
+
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
498
|
+
if (String(url).includes("/search")) return new Response("not found", { status: 404 });
|
|
499
|
+
return base(url, init);
|
|
500
|
+
}) as unknown as typeof fetch;
|
|
501
|
+
const found = await fetchRepoFiles("https://codeberg.org/acme/monorepo", { fetchImpl: impl, maxFiles: 1000 });
|
|
502
|
+
const paths = found.map((f) => f.path);
|
|
503
|
+
expect(paths).toContain(".forgejo/workflows/ci.yml");
|
|
504
|
+
expect(paths).toContain("charts/app/Chart.yaml");
|
|
505
|
+
});
|
|
506
|
+
|
|
294
507
|
test("an empty repo returns []", async () => {
|
|
295
508
|
const { impl } = gitTreeMock({});
|
|
296
509
|
const files = await fetchRepoFiles("https://github.com/acme/empty", { fetchImpl: impl });
|