@stsepelin/checktrail 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +355 -0
- package/SECURITY.md +46 -0
- package/dist/src/actionlint-evidence.d.ts +2 -0
- package/dist/src/actionlint-evidence.js +166 -0
- package/dist/src/actionlint-inputs.d.ts +5 -0
- package/dist/src/actionlint-inputs.js +123 -0
- package/dist/src/actionlint-runner.d.ts +1 -0
- package/dist/src/actionlint-runner.js +122 -0
- package/dist/src/actionlint.d.ts +19 -0
- package/dist/src/actionlint.js +80 -0
- package/dist/src/adapters.d.ts +49 -0
- package/dist/src/adapters.js +430 -0
- package/dist/src/architecture.d.ts +107 -0
- package/dist/src/architecture.js +230 -0
- package/dist/src/clang-evidence.d.ts +2 -0
- package/dist/src/clang-evidence.js +194 -0
- package/dist/src/clang-protocol.d.ts +3 -0
- package/dist/src/clang-protocol.js +25 -0
- package/dist/src/clang-runner.d.ts +1 -0
- package/dist/src/clang-runner.js +159 -0
- package/dist/src/clang.d.ts +44 -0
- package/dist/src/clang.js +197 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +338 -0
- package/dist/src/config.d.ts +33 -0
- package/dist/src/config.js +140 -0
- package/dist/src/contract-schema.d.ts +85 -0
- package/dist/src/contract-schema.js +56 -0
- package/dist/src/contract-worker.d.ts +1 -0
- package/dist/src/contract-worker.js +123 -0
- package/dist/src/contracts.d.ts +22 -0
- package/dist/src/contracts.js +123 -0
- package/dist/src/django-evidence.d.ts +2 -0
- package/dist/src/django-evidence.js +100 -0
- package/dist/src/django-runner.d.ts +1 -0
- package/dist/src/django-runner.js +80 -0
- package/dist/src/django.d.ts +9 -0
- package/dist/src/django.js +50 -0
- package/dist/src/dotnet-compiler.d.ts +1 -0
- package/dist/src/dotnet-compiler.js +87 -0
- package/dist/src/dotnet-evidence.d.ts +2 -0
- package/dist/src/dotnet-evidence.js +127 -0
- package/dist/src/dotnet-runner.d.ts +1 -0
- package/dist/src/dotnet-runner.js +167 -0
- package/dist/src/dotnet.d.ts +65 -0
- package/dist/src/dotnet.js +120 -0
- package/dist/src/engine.d.ts +19 -0
- package/dist/src/engine.js +239 -0
- package/dist/src/environment.d.ts +6 -0
- package/dist/src/environment.js +36 -0
- package/dist/src/eslint-evidence.d.ts +2 -0
- package/dist/src/eslint-evidence.js +98 -0
- package/dist/src/eslint-runner.d.ts +1 -0
- package/dist/src/eslint-runner.js +61 -0
- package/dist/src/eslint.d.ts +2 -0
- package/dist/src/eslint.js +40 -0
- package/dist/src/evidence.d.ts +2 -0
- package/dist/src/evidence.js +313 -0
- package/dist/src/external-adapter.d.ts +164 -0
- package/dist/src/external-adapter.js +265 -0
- package/dist/src/external-evidence.d.ts +2 -0
- package/dist/src/external-evidence.js +127 -0
- package/dist/src/external-runner.d.ts +1 -0
- package/dist/src/external-runner.js +122 -0
- package/dist/src/fastapi-evidence.d.ts +2 -0
- package/dist/src/fastapi-evidence.js +126 -0
- package/dist/src/fastapi-runner.d.ts +1 -0
- package/dist/src/fastapi-runner.js +84 -0
- package/dist/src/fastapi.d.ts +10 -0
- package/dist/src/fastapi.js +52 -0
- package/dist/src/fetch-pack.d.ts +16 -0
- package/dist/src/fetch-pack.js +154 -0
- package/dist/src/finding-policy-schema.d.ts +146 -0
- package/dist/src/finding-policy-schema.js +70 -0
- package/dist/src/finding-policy.d.ts +62 -0
- package/dist/src/finding-policy.js +206 -0
- package/dist/src/git-selection.d.ts +3 -0
- package/dist/src/git-selection.js +229 -0
- package/dist/src/go-directives.d.ts +1 -0
- package/dist/src/go-directives.js +32 -0
- package/dist/src/go-scope.d.ts +11 -0
- package/dist/src/go-scope.js +122 -0
- package/dist/src/golangci-evidence.d.ts +2 -0
- package/dist/src/golangci-evidence.js +92 -0
- package/dist/src/golangci-runner.d.ts +1 -0
- package/dist/src/golangci-runner.js +124 -0
- package/dist/src/golangci.d.ts +2 -0
- package/dist/src/golangci.js +35 -0
- package/dist/src/guidance.d.ts +98 -0
- package/dist/src/guidance.js +247 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +16 -0
- package/dist/src/inventory.d.ts +4 -0
- package/dist/src/inventory.js +94 -0
- package/dist/src/java-compiler.d.ts +1 -0
- package/dist/src/java-compiler.js +110 -0
- package/dist/src/java-evidence.d.ts +2 -0
- package/dist/src/java-evidence.js +114 -0
- package/dist/src/java-runner.d.ts +1 -0
- package/dist/src/java-runner.js +79 -0
- package/dist/src/java.d.ts +25 -0
- package/dist/src/java.js +102 -0
- package/dist/src/jest-evidence.d.ts +2 -0
- package/dist/src/jest-evidence.js +117 -0
- package/dist/src/jest-runner.d.ts +1 -0
- package/dist/src/jest-runner.js +46 -0
- package/dist/src/jest.d.ts +2 -0
- package/dist/src/jest.js +36 -0
- package/dist/src/junit.d.ts +18 -0
- package/dist/src/junit.js +157 -0
- package/dist/src/laravel-evidence.d.ts +2 -0
- package/dist/src/laravel-evidence.js +175 -0
- package/dist/src/laravel-runner.d.ts +1 -0
- package/dist/src/laravel-runner.js +168 -0
- package/dist/src/laravel.d.ts +8 -0
- package/dist/src/laravel.js +50 -0
- package/dist/src/local-tool.d.ts +1 -0
- package/dist/src/local-tool.js +19 -0
- package/dist/src/mcp.d.ts +14 -0
- package/dist/src/mcp.js +362 -0
- package/dist/src/mutation.d.ts +118 -0
- package/dist/src/mutation.js +370 -0
- package/dist/src/mypy-evidence.d.ts +2 -0
- package/dist/src/mypy-evidence.js +63 -0
- package/dist/src/mypy.d.ts +2 -0
- package/dist/src/mypy.js +59 -0
- package/dist/src/node-reporter.d.ts +1 -0
- package/dist/src/node-reporter.js +28 -0
- package/dist/src/nuxt-evidence.d.ts +2 -0
- package/dist/src/nuxt-evidence.js +160 -0
- package/dist/src/nuxt-protocol.d.ts +52 -0
- package/dist/src/nuxt-protocol.js +29 -0
- package/dist/src/nuxt-runner.d.ts +1 -0
- package/dist/src/nuxt-runner.js +165 -0
- package/dist/src/nuxt.d.ts +24 -0
- package/dist/src/nuxt.js +105 -0
- package/dist/src/output.d.ts +3 -0
- package/dist/src/output.js +48 -0
- package/dist/src/phpstan-evidence.d.ts +2 -0
- package/dist/src/phpstan-evidence.js +81 -0
- package/dist/src/phpstan.d.ts +2 -0
- package/dist/src/phpstan.js +51 -0
- package/dist/src/phpunit-evidence.d.ts +2 -0
- package/dist/src/phpunit-evidence.js +53 -0
- package/dist/src/phpunit.d.ts +2 -0
- package/dist/src/phpunit.js +58 -0
- package/dist/src/pint-evidence.d.ts +2 -0
- package/dist/src/pint-evidence.js +79 -0
- package/dist/src/pint-runner.d.ts +1 -0
- package/dist/src/pint-runner.js +53 -0
- package/dist/src/pint.d.ts +2 -0
- package/dist/src/pint.js +37 -0
- package/dist/src/playwright-evidence.d.ts +2 -0
- package/dist/src/playwright-evidence.js +127 -0
- package/dist/src/playwright-reporter.d.ts +10 -0
- package/dist/src/playwright-reporter.js +37 -0
- package/dist/src/playwright-runner.d.ts +1 -0
- package/dist/src/playwright-runner.js +61 -0
- package/dist/src/playwright.d.ts +2 -0
- package/dist/src/playwright.js +35 -0
- package/dist/src/policy-pack.d.ts +21 -0
- package/dist/src/policy-pack.js +41 -0
- package/dist/src/pytest-evidence.d.ts +2 -0
- package/dist/src/pytest-evidence.js +116 -0
- package/dist/src/pytest-runner.d.ts +1 -0
- package/dist/src/pytest-runner.js +48 -0
- package/dist/src/pytest.d.ts +2 -0
- package/dist/src/pytest.js +25 -0
- package/dist/src/report-validation.d.ts +129 -0
- package/dist/src/report-validation.js +27 -0
- package/dist/src/review.d.ts +270 -0
- package/dist/src/review.js +349 -0
- package/dist/src/ruby.d.ts +2 -0
- package/dist/src/ruby.js +25 -0
- package/dist/src/ruff-evidence.d.ts +2 -0
- package/dist/src/ruff-evidence.js +99 -0
- package/dist/src/ruff.d.ts +2 -0
- package/dist/src/ruff.js +40 -0
- package/dist/src/runner.d.ts +8 -0
- package/dist/src/runner.js +117 -0
- package/dist/src/runtime-inventory.d.ts +112 -0
- package/dist/src/runtime-inventory.js +200 -0
- package/dist/src/rust-dep-info.d.ts +1 -0
- package/dist/src/rust-dep-info.js +46 -0
- package/dist/src/rust-evidence.d.ts +2 -0
- package/dist/src/rust-evidence.js +157 -0
- package/dist/src/rust-runner.d.ts +1 -0
- package/dist/src/rust-runner.js +161 -0
- package/dist/src/rust.d.ts +7 -0
- package/dist/src/rust.js +39 -0
- package/dist/src/sarif.d.ts +86 -0
- package/dist/src/sarif.js +113 -0
- package/dist/src/schemas.d.ts +393 -0
- package/dist/src/schemas.js +222 -0
- package/dist/src/staticcheck-evidence.d.ts +2 -0
- package/dist/src/staticcheck-evidence.js +75 -0
- package/dist/src/swift.d.ts +2 -0
- package/dist/src/swift.js +20 -0
- package/dist/src/task-store.d.ts +37 -0
- package/dist/src/task-store.js +349 -0
- package/dist/src/tool-versions.d.ts +3 -0
- package/dist/src/tool-versions.js +260 -0
- package/dist/src/types.d.ts +157 -0
- package/dist/src/types.js +37 -0
- package/dist/src/typescript-build-evidence.d.ts +2 -0
- package/dist/src/typescript-build-evidence.js +86 -0
- package/dist/src/typescript-build-runner.d.ts +1 -0
- package/dist/src/typescript-build-runner.js +132 -0
- package/dist/src/typescript-build.d.ts +2 -0
- package/dist/src/typescript-build.js +41 -0
- package/dist/src/typescript.d.ts +2 -0
- package/dist/src/typescript.js +54 -0
- package/dist/src/validation-task-worker.d.ts +1 -0
- package/dist/src/validation-task-worker.js +205 -0
- package/dist/src/validation-tasks-protocol.d.ts +69 -0
- package/dist/src/validation-tasks-protocol.js +57 -0
- package/dist/src/validation-tasks.d.ts +11 -0
- package/dist/src/validation-tasks.js +144 -0
- package/dist/src/vitest-evidence.d.ts +2 -0
- package/dist/src/vitest-evidence.js +112 -0
- package/dist/src/vitest-runner.d.ts +1 -0
- package/dist/src/vitest-runner.js +48 -0
- package/dist/src/vitest.d.ts +2 -0
- package/dist/src/vitest.js +36 -0
- package/dist/src/vue-router-capture.d.ts +42 -0
- package/dist/src/vue-router-capture.js +87 -0
- package/dist/src/vue-router-evidence.d.ts +2 -0
- package/dist/src/vue-router-evidence.js +126 -0
- package/dist/src/vue-router-protocol.d.ts +63 -0
- package/dist/src/vue-router-protocol.js +44 -0
- package/dist/src/vue-router-runner.d.ts +1 -0
- package/dist/src/vue-router-runner.js +88 -0
- package/dist/src/vue-router.d.ts +23 -0
- package/dist/src/vue-router.js +108 -0
- package/dist/src/vue-tsc-runner.d.ts +1 -0
- package/dist/src/vue-tsc-runner.js +24 -0
- package/dist/src/workspace.d.ts +7 -0
- package/dist/src/workspace.js +53 -0
- package/docs/ACCEPTANCE.md +56 -0
- package/docs/ACTIONLINT.md +112 -0
- package/docs/ARCHITECTURE-POLICY.md +82 -0
- package/docs/ARCHITECTURE.md +122 -0
- package/docs/CLANG.md +113 -0
- package/docs/CLIENTS.md +109 -0
- package/docs/CONTRACTS.md +73 -0
- package/docs/DEPENDENCIES.md +42 -0
- package/docs/DJANGO.md +70 -0
- package/docs/DOTNET.md +114 -0
- package/docs/ENVIRONMENTS.md +52 -0
- package/docs/ESLINT.md +68 -0
- package/docs/EVALUATION.md +124 -0
- package/docs/EXECUTION.md +250 -0
- package/docs/EXTERNAL-ADAPTERS.md +170 -0
- package/docs/EXTERNAL-EVALUATION.md +155 -0
- package/docs/EXTERNAL-RUFF-EVALUATION.md +85 -0
- package/docs/FASTAPI.md +73 -0
- package/docs/FINDING-POLICY.md +67 -0
- package/docs/GO-RACE.md +19 -0
- package/docs/GO-SCOPE.md +63 -0
- package/docs/GOLANGCI-LINT.md +64 -0
- package/docs/GUIDANCE.md +46 -0
- package/docs/IMPACT-MEASUREMENT.md +97 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/JAVA.md +79 -0
- package/docs/JEST.md +36 -0
- package/docs/JUNIT.md +31 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/LARAVEL.md +111 -0
- package/docs/MCP-COMPATIBILITY.md +68 -0
- package/docs/MUTATIONS.md +93 -0
- package/docs/MYPY.md +37 -0
- package/docs/NATIVE-CI.md +96 -0
- package/docs/NUXT.md +123 -0
- package/docs/PACK-DISTRIBUTION.md +98 -0
- package/docs/PERFORMANCE.md +71 -0
- package/docs/PEST.md +36 -0
- package/docs/PHPSTAN.md +48 -0
- package/docs/PHPUNIT.md +32 -0
- package/docs/PINT.md +41 -0
- package/docs/PLAN.md +265 -0
- package/docs/PLAYWRIGHT.md +73 -0
- package/docs/POLICY-PACKS.md +106 -0
- package/docs/PRIOR-WORKFLOW-EVALUATION.md +116 -0
- package/docs/PUBLICATION.md +28 -0
- package/docs/PYTEST.md +51 -0
- package/docs/RELEASE.md +115 -0
- package/docs/RENAMING.md +55 -0
- package/docs/REVIEW-EXCHANGE.md +136 -0
- package/docs/RUBY.md +50 -0
- package/docs/RUFF.md +39 -0
- package/docs/RUNTIME-INVENTORY.md +56 -0
- package/docs/RUST.md +74 -0
- package/docs/SARIF.md +64 -0
- package/docs/STATUS.md +530 -0
- package/docs/SWIFT.md +48 -0
- package/docs/TASK-STORAGE.md +141 -0
- package/docs/TOOL-IDENTITY.md +30 -0
- package/docs/TYPESCRIPT-BUILD.md +55 -0
- package/docs/VALIDATION-TASKS.md +119 -0
- package/docs/VITEST.md +36 -0
- package/docs/VUE-ROUTER.md +130 -0
- package/docs/VUE-TSC.md +26 -0
- package/docs/WORKSPACES.md +81 -0
- package/docs/licenses/nodable-entities-3.0.0.txt +21 -0
- package/docs/measurements/client-claude-before-schema-fix-darwin-arm64-node26.json +60 -0
- package/docs/measurements/client-claude-darwin-arm64-node26.json +55 -0
- package/docs/measurements/client-codex-darwin-arm64-node26.json +175 -0
- package/docs/measurements/evaluation-corpus-repo-verifier.json +332 -0
- package/docs/measurements/evaluation-darwin-arm64-node26.json +1236 -0
- package/docs/measurements/evaluation-linux-arm64-node22.json +1236 -0
- package/docs/measurements/external-eslint-darwin-arm64-node26.json +11866 -0
- package/docs/measurements/external-eslint-linux-arm64-node22.json +11866 -0
- package/docs/measurements/external-eslint-preservation.json +1527 -0
- package/docs/measurements/external-ruff-darwin-arm64-node26.json +1031 -0
- package/docs/measurements/external-ruff-linux-arm64-node22.json +1031 -0
- package/docs/measurements/hosted-ci-52ba415.json +1005 -0
- package/docs/measurements/impact-darwin-arm64-node26.json +6685 -0
- package/docs/measurements/impact-linux-arm64-node22.json +6690 -0
- package/docs/measurements/performance-darwin-arm64-node26.json +845 -0
- package/docs/measurements/performance-linux-arm64-node22.json +845 -0
- package/docs/measurements/required-native-profiles.json +500 -0
- package/package.json +72 -0
- package/packs/actionlint.json +8 -0
- package/packs/cpp.json +8 -0
- package/packs/dotnet.json +8 -0
- package/packs/go.json +7 -0
- package/packs/java.json +8 -0
- package/packs/javascript-node.json +7 -0
- package/packs/nuxt.json +7 -0
- package/packs/php-pest.json +7 -0
- package/packs/php-phpunit.json +7 -0
- package/packs/python-pytest.json +7 -0
- package/packs/ruby.json +7 -0
- package/packs/rust.json +7 -0
- package/packs/swift.json +7 -0
- package/packs/vue-router.json +7 -0
- package/packs/vue.json +11 -0
- package/schemas/actionlint-config.schema.json +19 -0
- package/schemas/architecture-policy.schema.json +51 -0
- package/schemas/architecture-report.schema.json +123 -0
- package/schemas/architecture-summary.schema.json +82 -0
- package/schemas/clang-database.schema.json +23 -0
- package/schemas/config.schema.json +69 -0
- package/schemas/contract-bundle.schema.json +78 -0
- package/schemas/contract-report.schema.json +114 -0
- package/schemas/contract-summary.schema.json +72 -0
- package/schemas/dependency-graph.schema.json +65 -0
- package/schemas/django-config.schema.json +16 -0
- package/schemas/dotnet-config.schema.json +60 -0
- package/schemas/external-manifest.schema.json +97 -0
- package/schemas/external-reference.schema.json +10 -0
- package/schemas/external-request.schema.json +56 -0
- package/schemas/external-result.schema.json +111 -0
- package/schemas/fastapi-config.schema.json +27 -0
- package/schemas/finding-baseline.schema.json +92 -0
- package/schemas/finding-comparison-summary.schema.json +95 -0
- package/schemas/finding-comparison.schema.json +121 -0
- package/schemas/guidance-context.schema.json +34 -0
- package/schemas/guidance-report.schema.json +176 -0
- package/schemas/guidance-summary.schema.json +68 -0
- package/schemas/java-config.schema.json +24 -0
- package/schemas/junit.schema.json +69 -0
- package/schemas/laravel-config.schema.json +11 -0
- package/schemas/mutation-recipe.schema.json +31 -0
- package/schemas/mutation-report.schema.json +238 -0
- package/schemas/mutation-summary.schema.json +98 -0
- package/schemas/nuxt-config.schema.json +51 -0
- package/schemas/plan-summary.schema.json +63 -0
- package/schemas/plan.schema.json +244 -0
- package/schemas/policy-pack.schema.json +41 -0
- package/schemas/report-summary.schema.json +100 -0
- package/schemas/report.schema.json +463 -0
- package/schemas/review-assessment.schema.json +133 -0
- package/schemas/review-context-summary.schema.json +31 -0
- package/schemas/review-context.schema.json +254 -0
- package/schemas/review-receipt-summary.schema.json +104 -0
- package/schemas/review-receipt.schema.json +271 -0
- package/schemas/review-selection.schema.json +30 -0
- package/schemas/runtime-comparison-summary.schema.json +68 -0
- package/schemas/runtime-comparison.schema.json +127 -0
- package/schemas/runtime-inventory.schema.json +99 -0
- package/schemas/vue-router-config.schema.json +67 -0
- package/server.json +47 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { withinRoot } from "./inventory.js";
|
|
7
|
+
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
8
|
+
const identifier = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
9
|
+
const relativePath = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(4096)
|
|
13
|
+
.refine((value) => !path.posix.isAbsolute(value) &&
|
|
14
|
+
path.posix.normalize(value) === value &&
|
|
15
|
+
value !== "." &&
|
|
16
|
+
value !== ".." &&
|
|
17
|
+
!value.startsWith("../") &&
|
|
18
|
+
[...value].every((character) => character.charCodeAt(0) >= 32 &&
|
|
19
|
+
character.charCodeAt(0) !== 127 &&
|
|
20
|
+
character !== "\\"));
|
|
21
|
+
export const externalReferenceSchema = z.strictObject({
|
|
22
|
+
path: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(4096)
|
|
26
|
+
.refine((value) => path.isAbsolute(value) && !value.includes("\0")),
|
|
27
|
+
sha256,
|
|
28
|
+
});
|
|
29
|
+
export const externalReferencesSchema = z.array(externalReferenceSchema).max(8);
|
|
30
|
+
export const externalIdentitySchema = z.strictObject({
|
|
31
|
+
id: z.string().regex(/^external\.[a-z][a-z0-9-]{0,63}$/),
|
|
32
|
+
version: z
|
|
33
|
+
.string()
|
|
34
|
+
.regex(/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/)
|
|
35
|
+
.max(128),
|
|
36
|
+
sha256,
|
|
37
|
+
});
|
|
38
|
+
export const externalManifestSchema = z.strictObject({
|
|
39
|
+
schemaVersion: z.literal(1),
|
|
40
|
+
id: externalIdentitySchema.shape.id,
|
|
41
|
+
version: externalIdentitySchema.shape.version,
|
|
42
|
+
description: z.string().min(1).max(4096),
|
|
43
|
+
runtime: z.enum(["node", "python3", "php", "native"]),
|
|
44
|
+
entry: relativePath,
|
|
45
|
+
files: z
|
|
46
|
+
.array(z.strictObject({ path: relativePath, sha256 }))
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(512),
|
|
49
|
+
markers: z
|
|
50
|
+
.array(z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(/^[A-Za-z0-9_.-]{1,128}$/)
|
|
53
|
+
.refine((value) => value !== "." && value !== ".."))
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(32),
|
|
56
|
+
checks: z
|
|
57
|
+
.array(z.strictObject({
|
|
58
|
+
id: identifier,
|
|
59
|
+
kind: z.enum(["analysis", "syntax", "format", "test"]),
|
|
60
|
+
description: z.string().min(1).max(4096),
|
|
61
|
+
failOn: z.enum(["error", "warning"]),
|
|
62
|
+
scope: z.strictObject({
|
|
63
|
+
extensions: z
|
|
64
|
+
.array(z.string().regex(/^\.[A-Za-z0-9_.-]{1,64}$/))
|
|
65
|
+
.max(32),
|
|
66
|
+
names: z.array(z.string().regex(/^[A-Za-z0-9_.-]{1,128}$/)).max(32),
|
|
67
|
+
}),
|
|
68
|
+
}))
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(32),
|
|
71
|
+
});
|
|
72
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
73
|
+
export async function loadExternalAdapter(input, retainBytes = false) {
|
|
74
|
+
const reference = externalReferenceSchema.parse(input);
|
|
75
|
+
const directory = await realpath(path.dirname(reference.path));
|
|
76
|
+
const manifestFile = path.join(directory, path.basename(reference.path));
|
|
77
|
+
const manifestInfo = await lstat(manifestFile);
|
|
78
|
+
if (!manifestInfo.isFile() || manifestInfo.size > 256 * 1024)
|
|
79
|
+
throw new Error("External adapter manifest must be a regular file within 256 KiB");
|
|
80
|
+
const bytes = await readFile(manifestFile);
|
|
81
|
+
if (bytes.length > 256 * 1024 || hash(bytes) !== reference.sha256)
|
|
82
|
+
throw new Error("External adapter manifest integrity mismatch");
|
|
83
|
+
const manifest = externalManifestSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
84
|
+
if (new Set(manifest.files.map((file) => file.path)).size !==
|
|
85
|
+
manifest.files.length ||
|
|
86
|
+
new Set(manifest.markers).size !== manifest.markers.length ||
|
|
87
|
+
new Set(manifest.checks.map((check) => check.id)).size !==
|
|
88
|
+
manifest.checks.length)
|
|
89
|
+
throw new Error("Duplicate external adapter declarations");
|
|
90
|
+
if (!manifest.files.some((file) => file.path === manifest.entry))
|
|
91
|
+
throw new Error("External adapter entry must be a pinned file");
|
|
92
|
+
for (const check of manifest.checks) {
|
|
93
|
+
if (!check.scope.extensions.length && !check.scope.names.length)
|
|
94
|
+
throw new Error("External checks require an explicit nonempty scope selector");
|
|
95
|
+
if (new Set(check.scope.extensions).size !== check.scope.extensions.length ||
|
|
96
|
+
new Set(check.scope.names).size !== check.scope.names.length)
|
|
97
|
+
throw new Error("Duplicate external scope selector");
|
|
98
|
+
}
|
|
99
|
+
const contents = new Map();
|
|
100
|
+
let total = 0;
|
|
101
|
+
for (const file of manifest.files) {
|
|
102
|
+
const resolved = await withinRoot(directory, file.path);
|
|
103
|
+
if (resolved !== path.resolve(directory, file.path) ||
|
|
104
|
+
resolved === manifestFile)
|
|
105
|
+
throw new Error("External adapter files must be separate regular files without symbolic links");
|
|
106
|
+
const info = await lstat(resolved);
|
|
107
|
+
if (!info.isFile() || info.size > 32 * 1024 * 1024)
|
|
108
|
+
throw new Error("External adapter file exceeds 32 MiB");
|
|
109
|
+
const content = await readFile(resolved);
|
|
110
|
+
total += content.length;
|
|
111
|
+
if (content.length > 32 * 1024 * 1024 || total > 128 * 1024 * 1024)
|
|
112
|
+
throw new Error("External adapter bundle exceeds byte limits");
|
|
113
|
+
if (hash(content) !== file.sha256)
|
|
114
|
+
throw new Error("External adapter file integrity mismatch");
|
|
115
|
+
if (retainBytes)
|
|
116
|
+
contents.set(file.path, content);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
reference: { path: manifestFile, sha256: reference.sha256 },
|
|
120
|
+
manifest,
|
|
121
|
+
identity: {
|
|
122
|
+
id: manifest.id,
|
|
123
|
+
version: manifest.version,
|
|
124
|
+
sha256: reference.sha256,
|
|
125
|
+
},
|
|
126
|
+
contents,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function loadExternalAdapters(inputs = []) {
|
|
130
|
+
const result = [];
|
|
131
|
+
for (const reference of externalReferencesSchema.parse(inputs)) {
|
|
132
|
+
const adapter = await loadExternalAdapter(reference);
|
|
133
|
+
if (result.some((item) => item.identity.id === adapter.identity.id))
|
|
134
|
+
throw new Error("Duplicate external adapter identity");
|
|
135
|
+
result.push(adapter);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
export function externalPolicyFingerprint(policy, adapters) {
|
|
140
|
+
return adapters.length
|
|
141
|
+
? hash(JSON.stringify([
|
|
142
|
+
policy,
|
|
143
|
+
adapters
|
|
144
|
+
.map((item) => item.identity)
|
|
145
|
+
.sort((a, b) => a.id.localeCompare(b.id, "en")),
|
|
146
|
+
]))
|
|
147
|
+
: policy;
|
|
148
|
+
}
|
|
149
|
+
export function externalScope(project, check) {
|
|
150
|
+
return project.files.filter((file) => check.scope.names.includes(path.posix.basename(file)) ||
|
|
151
|
+
check.scope.extensions.some((extension) => file.endsWith(extension)));
|
|
152
|
+
}
|
|
153
|
+
export const externalInvocationSchema = z.strictObject({
|
|
154
|
+
reference: externalReferenceSchema,
|
|
155
|
+
identity: externalIdentitySchema,
|
|
156
|
+
project: z.string(),
|
|
157
|
+
runtime: z.enum(["node", "python3", "php", "native"]),
|
|
158
|
+
checkId: z.string(),
|
|
159
|
+
kind: z.enum(["analysis", "syntax", "format", "test"]),
|
|
160
|
+
failOn: z.enum(["error", "warning"]),
|
|
161
|
+
scope: z
|
|
162
|
+
.array(relativePath)
|
|
163
|
+
.min(1)
|
|
164
|
+
.max(20_000)
|
|
165
|
+
.refine((files) => new Set(files).size === files.length),
|
|
166
|
+
sourceFingerprint: sha256,
|
|
167
|
+
});
|
|
168
|
+
export const externalRequestSchema = externalInvocationSchema
|
|
169
|
+
.omit({ reference: true })
|
|
170
|
+
.extend({ protocolVersion: z.literal(1), root: z.string() });
|
|
171
|
+
export function externalChecks(source, project, adapter) {
|
|
172
|
+
return adapter.manifest.checks.map((definition) => {
|
|
173
|
+
const scope = externalScope(project, definition);
|
|
174
|
+
const check = {
|
|
175
|
+
id: `${adapter.identity.id}.${definition.id}`,
|
|
176
|
+
adapter: adapter.identity.id,
|
|
177
|
+
project: project.path,
|
|
178
|
+
scope,
|
|
179
|
+
kind: definition.kind,
|
|
180
|
+
parser: "external-json",
|
|
181
|
+
external: adapter.identity,
|
|
182
|
+
reason: definition.description,
|
|
183
|
+
commands: [],
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
const invocation = JSON.stringify(externalInvocationSchema.parse({
|
|
187
|
+
reference: adapter.reference,
|
|
188
|
+
identity: adapter.identity,
|
|
189
|
+
runtime: adapter.manifest.runtime,
|
|
190
|
+
project: project.path,
|
|
191
|
+
checkId: check.id,
|
|
192
|
+
kind: definition.kind,
|
|
193
|
+
failOn: definition.failOn,
|
|
194
|
+
scope,
|
|
195
|
+
sourceFingerprint: source.fingerprint,
|
|
196
|
+
}));
|
|
197
|
+
if (Buffer.byteLength(invocation) > 100 * 1024)
|
|
198
|
+
throw new Error("External adapter invocation exceeds 100 KiB");
|
|
199
|
+
check.commands.push({
|
|
200
|
+
temporaryDirectory: true,
|
|
201
|
+
executable: process.execPath,
|
|
202
|
+
args: [
|
|
203
|
+
fileURLToPath(new URL("./external-runner.js", import.meta.url)),
|
|
204
|
+
source.root,
|
|
205
|
+
invocation,
|
|
206
|
+
],
|
|
207
|
+
cwd: project.path,
|
|
208
|
+
env: {
|
|
209
|
+
PATH: process.env.PATH ?? "",
|
|
210
|
+
NODE_OPTIONS: "",
|
|
211
|
+
CHECKTRAIL_TEMP: "",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
check.unavailableReason =
|
|
217
|
+
error instanceof Error
|
|
218
|
+
? error.message
|
|
219
|
+
: "External adapter scope could not be prepared";
|
|
220
|
+
}
|
|
221
|
+
return check;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const count = z.number().int().nonnegative().max(10_000_000);
|
|
225
|
+
const testCounts = z.strictObject({
|
|
226
|
+
total: count,
|
|
227
|
+
passed: count,
|
|
228
|
+
failed: count,
|
|
229
|
+
skipped: count,
|
|
230
|
+
});
|
|
231
|
+
export const externalResultSchema = z.strictObject({
|
|
232
|
+
protocolVersion: z.literal(1),
|
|
233
|
+
identity: externalIdentitySchema,
|
|
234
|
+
checkId: z.string(),
|
|
235
|
+
sourceFingerprint: sha256,
|
|
236
|
+
files: z
|
|
237
|
+
.array(z.strictObject({
|
|
238
|
+
path: relativePath,
|
|
239
|
+
status: z.enum(["checked", "skipped", "unavailable"]),
|
|
240
|
+
tests: testCounts.optional(),
|
|
241
|
+
}))
|
|
242
|
+
.max(20_000),
|
|
243
|
+
findingsComplete: z.boolean(),
|
|
244
|
+
findings: z
|
|
245
|
+
.array(z.strictObject({
|
|
246
|
+
ruleId: z.string().min(1).max(256),
|
|
247
|
+
level: z.enum(["error", "warning", "note"]),
|
|
248
|
+
message: z.string().min(1).max(8192),
|
|
249
|
+
file: relativePath.optional(),
|
|
250
|
+
line: z.number().int().positive().optional(),
|
|
251
|
+
}))
|
|
252
|
+
.max(2000),
|
|
253
|
+
tools: z
|
|
254
|
+
.array(z.strictObject({
|
|
255
|
+
name: z.string().regex(/^[A-Za-z0-9_.-]{1,128}$/),
|
|
256
|
+
version: z
|
|
257
|
+
.string()
|
|
258
|
+
.min(1)
|
|
259
|
+
.max(256)
|
|
260
|
+
.refine((value) => [...value].every((character) => character.charCodeAt(0) >= 32 &&
|
|
261
|
+
character.charCodeAt(0) !== 127)),
|
|
262
|
+
}))
|
|
263
|
+
.min(1)
|
|
264
|
+
.max(32),
|
|
265
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { externalInvocationSchema, externalResultSchema, } from "./external-adapter.js";
|
|
4
|
+
const envelopeSchema = z.strictObject({
|
|
5
|
+
exitCode: z.number().int(),
|
|
6
|
+
stdout: z.string(),
|
|
7
|
+
stderr: z.string(),
|
|
8
|
+
});
|
|
9
|
+
export function externalEvidence(check, processes) {
|
|
10
|
+
const incomplete = {
|
|
11
|
+
status: "inconclusive",
|
|
12
|
+
reason: "External adapter evidence is malformed, contradictory or incomplete",
|
|
13
|
+
findingsComplete: false,
|
|
14
|
+
};
|
|
15
|
+
if (processes.length !== 1)
|
|
16
|
+
return incomplete;
|
|
17
|
+
const process = processes[0];
|
|
18
|
+
if (process.exitCode === 3 && !process.stderr) {
|
|
19
|
+
try {
|
|
20
|
+
z.strictObject({ unavailable: z.literal("external-runtime") }).parse(JSON.parse(process.stdout));
|
|
21
|
+
return {
|
|
22
|
+
status: "unavailable",
|
|
23
|
+
reason: "The external adapter runtime is unavailable",
|
|
24
|
+
findingsComplete: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return incomplete;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (process.exitCode !== 0 || process.stderr)
|
|
32
|
+
return {
|
|
33
|
+
status: "error",
|
|
34
|
+
reason: "External adapter execution did not complete",
|
|
35
|
+
findingsComplete: false,
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const envelope = envelopeSchema.parse(JSON.parse(process.stdout));
|
|
39
|
+
if (![0, 1].includes(envelope.exitCode))
|
|
40
|
+
return {
|
|
41
|
+
status: "error",
|
|
42
|
+
reason: "External adapter reported a tool or runtime error",
|
|
43
|
+
findingsComplete: false,
|
|
44
|
+
};
|
|
45
|
+
const result = externalResultSchema.parse(JSON.parse(envelope.stdout));
|
|
46
|
+
const invocation = externalInvocationSchema.parse(JSON.parse(check.commands[0].args[2]));
|
|
47
|
+
if (JSON.stringify(result.identity) !== JSON.stringify(check.external) ||
|
|
48
|
+
JSON.stringify(result.identity) !== JSON.stringify(invocation.identity) ||
|
|
49
|
+
result.checkId !== check.id ||
|
|
50
|
+
invocation.kind !== check.kind ||
|
|
51
|
+
result.sourceFingerprint !== invocation.sourceFingerprint ||
|
|
52
|
+
JSON.stringify(invocation.scope) !== JSON.stringify(check.scope))
|
|
53
|
+
return incomplete;
|
|
54
|
+
const expected = new Set(check.scope);
|
|
55
|
+
if (!expected.size ||
|
|
56
|
+
result.files.length !== expected.size ||
|
|
57
|
+
new Set(result.files.map((file) => file.path)).size !== expected.size ||
|
|
58
|
+
result.files.some((file) => !expected.has(file.path)) ||
|
|
59
|
+
result.findings.some((finding) => (finding.file && !expected.has(finding.file)) ||
|
|
60
|
+
(finding.line && !finding.file)) ||
|
|
61
|
+
new Set(result.tools.map((tool) => tool.name)).size !==
|
|
62
|
+
result.tools.length)
|
|
63
|
+
return incomplete;
|
|
64
|
+
const tests = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
65
|
+
let complete = result.findingsComplete &&
|
|
66
|
+
result.files.every((file) => file.status === "checked");
|
|
67
|
+
for (const file of result.files) {
|
|
68
|
+
if (check.kind !== "test") {
|
|
69
|
+
if (file.tests)
|
|
70
|
+
return incomplete;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!file.tests)
|
|
74
|
+
return incomplete;
|
|
75
|
+
const counts = file.tests;
|
|
76
|
+
if (counts.total !== counts.passed + counts.failed + counts.skipped)
|
|
77
|
+
return incomplete;
|
|
78
|
+
if (!counts.total ||
|
|
79
|
+
counts.skipped ||
|
|
80
|
+
counts.passed + counts.failed === 0)
|
|
81
|
+
complete = false;
|
|
82
|
+
for (const key of ["total", "passed", "failed", "skipped"])
|
|
83
|
+
tests[key] += counts[key];
|
|
84
|
+
}
|
|
85
|
+
const failed = tests.failed > 0 ||
|
|
86
|
+
result.findings.some((finding) => finding.level === "error" ||
|
|
87
|
+
(invocation.failOn === "warning" && finding.level === "warning"));
|
|
88
|
+
if (envelope.exitCode !== (failed ? 1 : 0))
|
|
89
|
+
return incomplete;
|
|
90
|
+
const evidence = {
|
|
91
|
+
external: {
|
|
92
|
+
...result.identity,
|
|
93
|
+
tools: result.tools.map((tool) => ({
|
|
94
|
+
...tool,
|
|
95
|
+
source: "adapter-reported",
|
|
96
|
+
})),
|
|
97
|
+
},
|
|
98
|
+
findings: result.findings.map((finding) => ({
|
|
99
|
+
ruleId: `${check.id}/${finding.ruleId}`,
|
|
100
|
+
level: finding.level,
|
|
101
|
+
message: finding.message,
|
|
102
|
+
...(finding.line ? { line: finding.line } : {}),
|
|
103
|
+
...(finding.file
|
|
104
|
+
? { file: path.posix.join(check.project, finding.file) }
|
|
105
|
+
: {}),
|
|
106
|
+
})),
|
|
107
|
+
findingsComplete: complete,
|
|
108
|
+
...(check.kind === "test" ? { tests } : {}),
|
|
109
|
+
};
|
|
110
|
+
if (failed)
|
|
111
|
+
return {
|
|
112
|
+
...evidence,
|
|
113
|
+
status: "failed",
|
|
114
|
+
reason: "The trusted external adapter reported failing diagnostics or tests",
|
|
115
|
+
};
|
|
116
|
+
if (!complete)
|
|
117
|
+
return { ...evidence, ...incomplete };
|
|
118
|
+
return {
|
|
119
|
+
...evidence,
|
|
120
|
+
status: "passed",
|
|
121
|
+
reason: "The trusted external adapter accounted for every planned file and completed its declared check",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return incomplete;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, realpath, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { externalInvocationSchema, externalReferenceSchema, externalRequestSchema, externalScope, loadExternalAdapter, } from "./external-adapter.js";
|
|
6
|
+
import { inventory, withinRoot } from "./inventory.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
if (process.argv[2] === "--version") {
|
|
9
|
+
const adapter = await loadExternalAdapter(externalReferenceSchema.parse(JSON.parse(process.argv[3])));
|
|
10
|
+
process.stdout.write(`${adapter.identity.version}\n`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const invocation = externalInvocationSchema.parse(JSON.parse(process.argv[3]));
|
|
14
|
+
const adapter = await loadExternalAdapter(invocation.reference, true);
|
|
15
|
+
if (JSON.stringify(adapter.identity) !== JSON.stringify(invocation.identity) ||
|
|
16
|
+
adapter.manifest.runtime !== invocation.runtime)
|
|
17
|
+
throw new Error("External adapter identity changed");
|
|
18
|
+
const definition = adapter.manifest.checks.find((check) => `${adapter.identity.id}.${check.id}` === invocation.checkId);
|
|
19
|
+
if (!definition ||
|
|
20
|
+
definition.kind !== invocation.kind ||
|
|
21
|
+
definition.failOn !== invocation.failOn)
|
|
22
|
+
throw new Error("External check definition changed");
|
|
23
|
+
const source = await inventory(process.argv[2]);
|
|
24
|
+
if (source.fingerprint !== invocation.sourceFingerprint)
|
|
25
|
+
throw new Error("External check source changed before execution");
|
|
26
|
+
const cwd = await withinRoot(source.root, invocation.project);
|
|
27
|
+
if (cwd !== (await realpath(process.cwd())))
|
|
28
|
+
throw new Error("External adapter project does not match its working directory");
|
|
29
|
+
for (const file of invocation.scope) {
|
|
30
|
+
const relative = path.posix.join(invocation.project, file);
|
|
31
|
+
if (!source.files.includes(relative) ||
|
|
32
|
+
(await withinRoot(source.root, relative)) !==
|
|
33
|
+
path.resolve(source.root, relative))
|
|
34
|
+
throw new Error("External scope contains missing or symbolic-link inputs");
|
|
35
|
+
}
|
|
36
|
+
if (JSON.stringify(externalScope({
|
|
37
|
+
path: invocation.project,
|
|
38
|
+
adapter: adapter.identity.id,
|
|
39
|
+
markers: [],
|
|
40
|
+
files: invocation.scope,
|
|
41
|
+
}, definition)) !== JSON.stringify(invocation.scope))
|
|
42
|
+
throw new Error("External scope does not match its declared selectors");
|
|
43
|
+
const temporary = process.env.CHECKTRAIL_TEMP ||
|
|
44
|
+
(await mkdtemp(path.join(tmpdir(), "checktrail-external-")));
|
|
45
|
+
try {
|
|
46
|
+
const bundle = path.join(temporary, "bundle");
|
|
47
|
+
for (const [file, content] of adapter.contents) {
|
|
48
|
+
const output = path.join(bundle, file);
|
|
49
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
50
|
+
await writeFile(output, content, { flag: "wx", mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
const entry = path.join(bundle, adapter.manifest.entry);
|
|
53
|
+
const request = path.join(temporary, "request.json");
|
|
54
|
+
await writeFile(request, JSON.stringify(externalRequestSchema.parse({
|
|
55
|
+
protocolVersion: 1,
|
|
56
|
+
root: source.root,
|
|
57
|
+
identity: invocation.identity,
|
|
58
|
+
project: invocation.project,
|
|
59
|
+
runtime: invocation.runtime,
|
|
60
|
+
checkId: invocation.checkId,
|
|
61
|
+
kind: invocation.kind,
|
|
62
|
+
failOn: invocation.failOn,
|
|
63
|
+
scope: invocation.scope,
|
|
64
|
+
sourceFingerprint: invocation.sourceFingerprint,
|
|
65
|
+
})), { mode: 0o600 });
|
|
66
|
+
let executable;
|
|
67
|
+
let args;
|
|
68
|
+
switch (adapter.manifest.runtime) {
|
|
69
|
+
case "node":
|
|
70
|
+
executable = process.execPath;
|
|
71
|
+
args = [entry, request];
|
|
72
|
+
break;
|
|
73
|
+
case "python3":
|
|
74
|
+
executable = "python3";
|
|
75
|
+
args = ["-I", "-B", entry, request];
|
|
76
|
+
break;
|
|
77
|
+
case "php":
|
|
78
|
+
executable = "php";
|
|
79
|
+
args = ["-n", entry, request];
|
|
80
|
+
break;
|
|
81
|
+
case "native":
|
|
82
|
+
executable = entry;
|
|
83
|
+
args = [request];
|
|
84
|
+
await chmod(entry, 0o700);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
const result = spawnSync(executable, args, {
|
|
88
|
+
cwd,
|
|
89
|
+
env: { ...process.env, NODE_OPTIONS: "" },
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
maxBuffer: 1024 * 1024,
|
|
92
|
+
});
|
|
93
|
+
if (result.error &&
|
|
94
|
+
result.error.code === "ENOENT") {
|
|
95
|
+
process.stdout.write(JSON.stringify({ unavailable: "external-runtime" }));
|
|
96
|
+
process.exitCode = 3;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (result.error ||
|
|
100
|
+
result.signal ||
|
|
101
|
+
result.status === null ||
|
|
102
|
+
Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr) >
|
|
103
|
+
1024 * 1024)
|
|
104
|
+
throw new Error("External adapter process did not complete within evidence bounds");
|
|
105
|
+
await loadExternalAdapter(invocation.reference);
|
|
106
|
+
if ((await inventory(source.root)).fingerprint !==
|
|
107
|
+
invocation.sourceFingerprint)
|
|
108
|
+
throw new Error("External check source changed during execution");
|
|
109
|
+
process.stdout.write(JSON.stringify({
|
|
110
|
+
exitCode: result.status,
|
|
111
|
+
stdout: result.stdout,
|
|
112
|
+
stderr: result.stderr,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await rm(temporary, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
main().catch(() => {
|
|
120
|
+
process.stderr.write("External adapter evidence collection could not complete\n");
|
|
121
|
+
process.exitCode = 2;
|
|
122
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runtimeInventorySchema } from "./runtime-inventory.js";
|
|
3
|
+
const count = z.number().int().nonnegative();
|
|
4
|
+
const schema = z.strictObject({
|
|
5
|
+
version: z.literal(1),
|
|
6
|
+
versions: z.strictObject({
|
|
7
|
+
fastapi: z.literal("0.141.1"),
|
|
8
|
+
starlette: z.literal("1.6.0"),
|
|
9
|
+
}),
|
|
10
|
+
totalRoutes: count,
|
|
11
|
+
supportedRoutes: count,
|
|
12
|
+
applicationRoutes: count,
|
|
13
|
+
entryRouteIndices: z.array(count),
|
|
14
|
+
runtime: runtimeInventorySchema,
|
|
15
|
+
});
|
|
16
|
+
const attributesSchema = z.strictObject({
|
|
17
|
+
protocol: z.enum(["http", "websocket"]),
|
|
18
|
+
method: z.string().regex(/^[A-Z]+$/),
|
|
19
|
+
path: z.string().startsWith("/"),
|
|
20
|
+
handler: z.string().min(1),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
dependencies: z.array(z.string().min(1)),
|
|
23
|
+
includeInSchema: z.boolean(),
|
|
24
|
+
});
|
|
25
|
+
export function fastapiEvidence(check, processes) {
|
|
26
|
+
const incomplete = {
|
|
27
|
+
status: "inconclusive",
|
|
28
|
+
reason: "FastAPI runtime routes are unsupported, empty, malformed or incomplete.",
|
|
29
|
+
};
|
|
30
|
+
if (processes.length !== 1)
|
|
31
|
+
return incomplete;
|
|
32
|
+
if (processes[0].exitCode === 3) {
|
|
33
|
+
try {
|
|
34
|
+
z.strictObject({
|
|
35
|
+
unavailable: z.literal("fastapi-runtime"),
|
|
36
|
+
reason: z.enum(["missing-package", "unsupported-version"]),
|
|
37
|
+
}).parse(JSON.parse(processes[0].stdout));
|
|
38
|
+
return {
|
|
39
|
+
status: "unavailable",
|
|
40
|
+
reason: "The pinned FastAPI/Starlette runtime profile is not installed.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return incomplete;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (processes[0].exitCode !== 0)
|
|
48
|
+
return {
|
|
49
|
+
status: "error",
|
|
50
|
+
reason: "FastAPI application, lifespan or supported collector could not complete.",
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
const result = schema.parse(JSON.parse(processes[0].stdout));
|
|
54
|
+
const profile = JSON.parse(check.commands[0].args[2]);
|
|
55
|
+
const routes = result.runtime.collections[0];
|
|
56
|
+
if (result.runtime.sourceFingerprint !== check.commands[0]?.args[3] ||
|
|
57
|
+
result.runtime.collections.length !== 1 ||
|
|
58
|
+
routes?.kind !== "routes" ||
|
|
59
|
+
!routes.ordered ||
|
|
60
|
+
routes.complete !== (result.supportedRoutes === result.totalRoutes) ||
|
|
61
|
+
result.supportedRoutes > result.totalRoutes ||
|
|
62
|
+
result.applicationRoutes > result.supportedRoutes ||
|
|
63
|
+
routes.entries.length < result.supportedRoutes ||
|
|
64
|
+
result.entryRouteIndices.length !== routes.entries.length ||
|
|
65
|
+
new Set(result.entryRouteIndices).size !== result.supportedRoutes ||
|
|
66
|
+
result.entryRouteIndices.some((index) => index >= result.totalRoutes) ||
|
|
67
|
+
result.entryRouteIndices.some((index, offset) => offset > 0 && index < result.entryRouteIndices[offset - 1]) ||
|
|
68
|
+
result.runtime.producer.name !== "checktrail.fastapi-routes" ||
|
|
69
|
+
result.runtime.producer.version !== "1.0.0" ||
|
|
70
|
+
result.runtime.assembly.name !== profile.assembly ||
|
|
71
|
+
result.runtime.assembly.environment !== profile.environment ||
|
|
72
|
+
!result.applicationRoutes)
|
|
73
|
+
return incomplete;
|
|
74
|
+
const identities = new Set();
|
|
75
|
+
let duplicates = 0;
|
|
76
|
+
const registrations = new Map();
|
|
77
|
+
const signatures = new Map();
|
|
78
|
+
for (const [index, entry] of routes.entries.entries()) {
|
|
79
|
+
const attributes = attributesSchema.parse(entry.attributes);
|
|
80
|
+
const key = attributes.protocol === "http"
|
|
81
|
+
? `HTTP ${attributes.method} ${attributes.path}`
|
|
82
|
+
: `WEBSOCKET ${attributes.path}`;
|
|
83
|
+
if (entry.key !== key ||
|
|
84
|
+
(attributes.protocol === "websocket" &&
|
|
85
|
+
attributes.method !== "WEBSOCKET"))
|
|
86
|
+
return incomplete;
|
|
87
|
+
const registration = result.entryRouteIndices[index];
|
|
88
|
+
const methods = registrations.get(registration) ?? new Set();
|
|
89
|
+
if (methods.has(attributes.method))
|
|
90
|
+
return incomplete;
|
|
91
|
+
const signature = JSON.stringify([
|
|
92
|
+
attributes.protocol,
|
|
93
|
+
attributes.path,
|
|
94
|
+
attributes.handler,
|
|
95
|
+
attributes.name,
|
|
96
|
+
attributes.dependencies,
|
|
97
|
+
attributes.includeInSchema,
|
|
98
|
+
]);
|
|
99
|
+
if (signatures.has(registration) &&
|
|
100
|
+
signatures.get(registration) !== signature)
|
|
101
|
+
return incomplete;
|
|
102
|
+
signatures.set(registration, signature);
|
|
103
|
+
methods.add(attributes.method);
|
|
104
|
+
registrations.set(registration, methods);
|
|
105
|
+
if (identities.has(entry.key))
|
|
106
|
+
duplicates++;
|
|
107
|
+
identities.add(entry.key);
|
|
108
|
+
}
|
|
109
|
+
if (duplicates)
|
|
110
|
+
return {
|
|
111
|
+
status: "failed",
|
|
112
|
+
reason: "FastAPI registered exact duplicate method/path routes; inspect the captured runtime inventory.",
|
|
113
|
+
runtime: result.runtime,
|
|
114
|
+
};
|
|
115
|
+
if (!routes.complete)
|
|
116
|
+
return { ...incomplete, runtime: result.runtime };
|
|
117
|
+
return {
|
|
118
|
+
status: "passed",
|
|
119
|
+
reason: "FastAPI lifespan completed and the supported flat route inventory has no exact duplicate method/path registrations.",
|
|
120
|
+
runtime: result.runtime,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return incomplete;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const fastapiRunner: string;
|