@yiaany/ghostapi 0.1.9 → 0.2.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/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
package/dist/evidence/report.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { lstat, mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
5
5
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
6
6
|
import { detectEgressCapabilities } from "../egress/capabilities.js";
|
|
7
|
-
import { evaluatePolicy, loadPolicyFile } from "../policy/index.js";
|
|
8
|
-
import { sanitizeSecrets, sanitizeSecretString, isSecretFieldName } from "../security/secrets.js";
|
|
9
|
-
import { atomicWriteJson, ensurePrivateDirectory } from "../storage/fileStore.js";
|
|
10
|
-
import { diffContracts, loadContract } from "../contracts/index.js";
|
|
7
|
+
import { evaluatePolicy, loadPolicyFile, } from "../policy/index.js";
|
|
8
|
+
import { sanitizeSecrets, sanitizeSecretString, isSecretFieldName, } from "../security/secrets.js";
|
|
9
|
+
import { atomicWriteJson, ensurePrivateDirectory, } from "../storage/fileStore.js";
|
|
10
|
+
import { diffContracts, loadContract, } from "../contracts/index.js";
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const packageJson = require("../../package.json");
|
|
13
13
|
const MAX_EVENTS_FOR_REPORT = 1_000;
|
|
@@ -16,7 +16,19 @@ const MAX_REPORT_BYTES = 512 * 1024;
|
|
|
16
16
|
const MAX_RUN_EVIDENCE_BYTES = 128 * 1024;
|
|
17
17
|
const MAX_REPORTS_RETAINED = 20;
|
|
18
18
|
const MAX_ATTEMPTS = 500;
|
|
19
|
-
const SENSITIVE_QUERY_KEYS = new Set([
|
|
19
|
+
const SENSITIVE_QUERY_KEYS = new Set([
|
|
20
|
+
"api_key",
|
|
21
|
+
"apikey",
|
|
22
|
+
"access_token",
|
|
23
|
+
"refresh_token",
|
|
24
|
+
"token",
|
|
25
|
+
"secret",
|
|
26
|
+
"key",
|
|
27
|
+
"client_secret",
|
|
28
|
+
"password",
|
|
29
|
+
"authorization",
|
|
30
|
+
"cookie",
|
|
31
|
+
]);
|
|
20
32
|
export class EvidenceReportError extends Error {
|
|
21
33
|
constructor(message) {
|
|
22
34
|
super(message);
|
|
@@ -33,21 +45,27 @@ export async function generateEvidenceReport(options = {}) {
|
|
|
33
45
|
events,
|
|
34
46
|
policy: loadedPolicy?.policy,
|
|
35
47
|
policyHash: loadedPolicy?.hash ?? readString(run?.evidence.policy?.policyHash),
|
|
36
|
-
requiredScenarios: loadedPolicy?.policy.requiredScenarios ??
|
|
48
|
+
requiredScenarios: loadedPolicy?.policy.requiredScenarios ??
|
|
49
|
+
readStringArray(run?.evidence.policy?.requiredScenarios),
|
|
37
50
|
runEvidence: run?.evidence ?? null,
|
|
38
51
|
contractDiff,
|
|
39
52
|
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
40
|
-
ghostApiVersion: options.ghostApiVersion ?? readPackageVersion()
|
|
53
|
+
ghostApiVersion: options.ghostApiVersion ?? readPackageVersion(),
|
|
41
54
|
});
|
|
42
55
|
const path = await writeEvidenceReport(report, projectRoot, options.outPath);
|
|
43
56
|
return { report, path };
|
|
44
57
|
}
|
|
45
58
|
export function buildEvidenceReport(input) {
|
|
46
|
-
const sanitizedEvents = input.events
|
|
59
|
+
const sanitizedEvents = input.events
|
|
60
|
+
.slice(0, MAX_EVENTS_FOR_REPORT)
|
|
61
|
+
.map((event) => sanitizeEvent(event));
|
|
47
62
|
const capability = detectEgressCapabilities();
|
|
48
|
-
const runEvents = Array.isArray(input.runEvidence?.events)
|
|
63
|
+
const runEvents = Array.isArray(input.runEvidence?.events)
|
|
64
|
+
? input.runEvidence.events
|
|
65
|
+
: [];
|
|
49
66
|
const runStatus = readRunStatus(input.runEvidence?.status);
|
|
50
|
-
const runUsedNamespace = input.runEvidence?.backend === "linux-network-namespace" &&
|
|
67
|
+
const runUsedNamespace = input.runEvidence?.backend === "linux-network-namespace" &&
|
|
68
|
+
runStatus === "finished";
|
|
51
69
|
const completedScenarios = detectScenarios(sanitizedEvents);
|
|
52
70
|
const requiredScenarios = uniqueSorted(input.requiredScenarios ?? []);
|
|
53
71
|
const providers = uniqueSorted(sanitizedEvents.map((event) => event.provider).filter(Boolean));
|
|
@@ -61,87 +79,183 @@ export function buildEvidenceReport(input) {
|
|
|
61
79
|
}
|
|
62
80
|
const runOutputSecretMatches = readNonNegativeInteger(input.runEvidence?.output?.secretMatches) ?? 0;
|
|
63
81
|
secretMatches += runOutputSecretMatches;
|
|
64
|
-
const allowedAttempts = sanitizedEvents
|
|
82
|
+
const allowedAttempts = sanitizedEvents
|
|
83
|
+
.map((event) => ({
|
|
65
84
|
provider: limitText(event.provider),
|
|
66
85
|
method: limitText(event.method),
|
|
67
86
|
path: sanitizePath(event.path),
|
|
68
87
|
statusCode: event.statusCode,
|
|
69
|
-
source: limitText(event.source)
|
|
70
|
-
}))
|
|
71
|
-
|
|
88
|
+
source: limitText(event.source),
|
|
89
|
+
}))
|
|
90
|
+
.slice(0, MAX_ATTEMPTS);
|
|
91
|
+
const failures = allowedAttempts
|
|
92
|
+
.filter((attempt) => attempt.statusCode >= 400)
|
|
93
|
+
.map((attempt) => ({
|
|
94
|
+
provider: attempt.provider,
|
|
95
|
+
method: attempt.method,
|
|
96
|
+
path: attempt.path,
|
|
97
|
+
statusCode: attempt.statusCode,
|
|
98
|
+
}));
|
|
72
99
|
const retryCount = sanitizedEvents.filter((event) => event.statusCode === 429 || hasRetryAfter(event)).length;
|
|
73
|
-
const productionAttempts =
|
|
100
|
+
const productionAttempts = null;
|
|
74
101
|
const contractDrift = input.contractDiff === undefined
|
|
75
102
|
? { breaking: 0, nonBreaking: 0, uncertain: 0 }
|
|
76
|
-
: {
|
|
103
|
+
: {
|
|
104
|
+
baselineHash: input.contractDiff.baselineHash,
|
|
105
|
+
candidateHash: input.contractDiff.candidateHash,
|
|
106
|
+
...input.contractDiff.summary,
|
|
107
|
+
};
|
|
77
108
|
const findings = [];
|
|
78
109
|
const warnings = [];
|
|
79
110
|
if (sanitizedEvents.length < input.events.length)
|
|
80
111
|
warnings.push(`Event evidence was truncated to ${MAX_EVENTS_FOR_REPORT} events.`);
|
|
81
112
|
if (input.runEvidence === null)
|
|
82
113
|
warnings.push("Run evidence is missing; report cannot prove launcher lifecycle or blocked kernel egress attempts.");
|
|
114
|
+
warnings.push("Production egress attempts are not measured; the current backend records namespace lifecycle and GhostAPI-local traffic only.");
|
|
83
115
|
if (readBoolean(input.runEvidence?.output?.timedOut))
|
|
84
116
|
warnings.push("Target exceeded the configured run timeout.");
|
|
85
117
|
if (readBoolean(input.runEvidence?.output?.limitExceeded))
|
|
86
118
|
warnings.push("Target exceeded the configured output limit.");
|
|
87
119
|
if (runUsedNamespace)
|
|
88
|
-
findings.push({
|
|
120
|
+
findings.push({
|
|
121
|
+
id: "enforcement.isolated",
|
|
122
|
+
severity: "pass",
|
|
123
|
+
message: "Run evidence reports a completed Linux namespace boundary.",
|
|
124
|
+
});
|
|
89
125
|
else
|
|
90
|
-
findings.push({
|
|
126
|
+
findings.push({
|
|
127
|
+
id: "enforcement.degraded",
|
|
128
|
+
severity: "warning",
|
|
129
|
+
message: "Egress enforcement is degraded or unavailable on this host.",
|
|
130
|
+
});
|
|
91
131
|
if (input.runEvidence?.status === "finished")
|
|
92
|
-
findings.push({
|
|
132
|
+
findings.push({
|
|
133
|
+
id: "run.finished",
|
|
134
|
+
severity: "pass",
|
|
135
|
+
message: "Run evidence reached finished status.",
|
|
136
|
+
});
|
|
93
137
|
else
|
|
94
|
-
findings.push({
|
|
138
|
+
findings.push({
|
|
139
|
+
id: "run.incomplete",
|
|
140
|
+
severity: "warning",
|
|
141
|
+
message: "Run evidence is absent or incomplete.",
|
|
142
|
+
});
|
|
95
143
|
for (const scenarioId of requiredScenarios) {
|
|
96
|
-
const decision = input.policy === undefined
|
|
144
|
+
const decision = input.policy === undefined
|
|
145
|
+
? { allowed: completedScenarios.includes(scenarioId) }
|
|
146
|
+
: evaluatePolicy(input.policy, {
|
|
147
|
+
type: "scenario",
|
|
148
|
+
scenarioId,
|
|
149
|
+
completedScenarioIds: completedScenarios,
|
|
150
|
+
});
|
|
97
151
|
findings.push(decision.allowed
|
|
98
|
-
? {
|
|
99
|
-
|
|
152
|
+
? {
|
|
153
|
+
id: `scenario.${scenarioId}`,
|
|
154
|
+
severity: "pass",
|
|
155
|
+
message: `Required scenario completed: ${scenarioId}`,
|
|
156
|
+
}
|
|
157
|
+
: {
|
|
158
|
+
id: `scenario.${scenarioId}`,
|
|
159
|
+
severity: "fail",
|
|
160
|
+
message: `Required scenario missing: ${scenarioId}`,
|
|
161
|
+
});
|
|
100
162
|
}
|
|
101
|
-
const reportDecision = input.policy === undefined
|
|
163
|
+
const reportDecision = input.policy === undefined
|
|
164
|
+
? { allowed: secretMatches === 0 && contractDrift.breaking === 0 }
|
|
165
|
+
: {
|
|
166
|
+
allowed: secretMatches <=
|
|
167
|
+
input.policy.reports.maxForbiddenCredentialMatches &&
|
|
168
|
+
contractDrift.breaking <=
|
|
169
|
+
input.policy.reports.maxBreakingContractChanges,
|
|
170
|
+
};
|
|
102
171
|
findings.push(reportDecision.allowed
|
|
103
|
-
? {
|
|
104
|
-
|
|
172
|
+
? {
|
|
173
|
+
id: "policy.report-thresholds",
|
|
174
|
+
severity: "pass",
|
|
175
|
+
message: "Policy report thresholds are satisfied.",
|
|
176
|
+
}
|
|
177
|
+
: {
|
|
178
|
+
id: "policy.report-thresholds",
|
|
179
|
+
severity: "fail",
|
|
180
|
+
message: "Policy report thresholds are exceeded.",
|
|
181
|
+
});
|
|
105
182
|
if (secretMatches > 0)
|
|
106
|
-
findings.push({
|
|
183
|
+
findings.push({
|
|
184
|
+
id: "secrets.detected",
|
|
185
|
+
severity: "fail",
|
|
186
|
+
message: "Secret-shaped values were detected and summarized without raw values.",
|
|
187
|
+
count: secretMatches,
|
|
188
|
+
});
|
|
107
189
|
if (input.contractDiff !== undefined) {
|
|
108
190
|
if (contractDrift.breaking > 0)
|
|
109
|
-
findings.push({
|
|
191
|
+
findings.push({
|
|
192
|
+
id: "contract-drift.breaking",
|
|
193
|
+
severity: "fail",
|
|
194
|
+
message: "Breaking contract drift was detected.",
|
|
195
|
+
count: contractDrift.breaking,
|
|
196
|
+
});
|
|
110
197
|
if (contractDrift.uncertain > 0)
|
|
111
|
-
findings.push({
|
|
198
|
+
findings.push({
|
|
199
|
+
id: "contract-drift.uncertain",
|
|
200
|
+
severity: "warning",
|
|
201
|
+
message: "Contract changes need compatibility review because client tolerance is unknown.",
|
|
202
|
+
count: contractDrift.uncertain,
|
|
203
|
+
});
|
|
112
204
|
}
|
|
113
205
|
if (failures.length > 0)
|
|
114
|
-
findings.push({
|
|
206
|
+
findings.push({
|
|
207
|
+
id: "provider.failures",
|
|
208
|
+
severity: "warning",
|
|
209
|
+
message: "Provider failures were observed.",
|
|
210
|
+
count: failures.length,
|
|
211
|
+
});
|
|
115
212
|
const reportWithoutHash = {
|
|
116
213
|
schemaVersion: 1,
|
|
117
|
-
artifact: {
|
|
214
|
+
artifact: {
|
|
215
|
+
generatedAt: input.generatedAt,
|
|
216
|
+
logicalHash: "",
|
|
217
|
+
canonicalization: "json-stable-sorted-keys-v1",
|
|
218
|
+
},
|
|
118
219
|
run: {
|
|
119
220
|
id: readString(input.runEvidence?.runId) ?? "unknown",
|
|
120
221
|
status: runStatus,
|
|
121
222
|
startedAt: firstTimestamp(runEvents),
|
|
122
223
|
finishedAt: lastTimestamp(runEvents),
|
|
123
|
-
exitCode: lastExitCode(runEvents)
|
|
224
|
+
exitCode: lastExitCode(runEvents),
|
|
124
225
|
},
|
|
125
226
|
ghostapi: { version: input.ghostApiVersion ?? "unknown" },
|
|
126
227
|
enforcement: {
|
|
127
228
|
capability: capability.summary,
|
|
128
|
-
mode: input.runEvidence?.backend === "linux-network-namespace"
|
|
229
|
+
mode: input.runEvidence?.backend === "linux-network-namespace"
|
|
230
|
+
? "linux-network-namespace"
|
|
231
|
+
: "proxy-guidance",
|
|
129
232
|
isolated: runUsedNamespace,
|
|
130
|
-
degraded: !runUsedNamespace
|
|
233
|
+
degraded: !runUsedNamespace,
|
|
131
234
|
},
|
|
132
235
|
policy: { hash: input.policyHash, requiredScenarios },
|
|
133
236
|
coverage: { providers, scenarios: completedScenarios },
|
|
134
237
|
contractDrift,
|
|
135
238
|
egress: { allowedAttempts, blockedAttempts: [], productionAttempts },
|
|
136
|
-
secrets: {
|
|
137
|
-
|
|
239
|
+
secrets: {
|
|
240
|
+
categories: [...secretCategories].sort(),
|
|
241
|
+
matches: secretMatches,
|
|
242
|
+
},
|
|
243
|
+
retriesAndFailures: {
|
|
244
|
+
retryCount,
|
|
245
|
+
failureCount: failures.length,
|
|
246
|
+
failures: failures.slice(0, MAX_ATTEMPTS),
|
|
247
|
+
},
|
|
138
248
|
findings: findings.sort((left, right) => left.id.localeCompare(right.id)),
|
|
139
249
|
warnings: warnings.map(limitText).sort(),
|
|
140
|
-
summary: { passed: false, failCount: 0, warningCount: 0 }
|
|
250
|
+
summary: { passed: false, failCount: 0, warningCount: 0 },
|
|
141
251
|
};
|
|
142
252
|
const failCount = reportWithoutHash.findings.filter((finding) => finding.severity === "fail").length;
|
|
143
253
|
const warningCount = reportWithoutHash.findings.filter((finding) => finding.severity === "warning").length + reportWithoutHash.warnings.length;
|
|
144
|
-
reportWithoutHash.summary = {
|
|
254
|
+
reportWithoutHash.summary = {
|
|
255
|
+
passed: failCount === 0,
|
|
256
|
+
failCount,
|
|
257
|
+
warningCount,
|
|
258
|
+
};
|
|
145
259
|
reportWithoutHash.artifact.logicalHash = hashLogicalReport(reportWithoutHash);
|
|
146
260
|
return reportWithoutHash;
|
|
147
261
|
}
|
|
@@ -179,13 +293,13 @@ export function formatEvidenceReport(report) {
|
|
|
179
293
|
`Contract drift: ${(report.contractDrift ?? emptyContractDrift()).breaking} breaking, ${(report.contractDrift ?? emptyContractDrift()).nonBreaking} non-breaking, ${(report.contractDrift ?? emptyContractDrift()).uncertain} uncertain`,
|
|
180
294
|
`Allowed egress attempts: ${report.egress.allowedAttempts.length}`,
|
|
181
295
|
`Blocked egress attempts: ${report.egress.blockedAttempts.length}`,
|
|
182
|
-
`Production egress attempts: ${report.egress.productionAttempts}`,
|
|
296
|
+
`Production egress attempts: ${report.egress.productionAttempts ?? "not measured"}`,
|
|
183
297
|
`Secret categories: ${report.secrets.categories.length > 0 ? report.secrets.categories.map(escapeTerminal).join(", ") : "none"}`,
|
|
184
298
|
`Retries: ${report.retriesAndFailures.retryCount}`,
|
|
185
299
|
`Failures: ${report.retriesAndFailures.failureCount}`,
|
|
186
300
|
`Findings: ${report.summary.failCount} fail, ${report.summary.warningCount} warning`,
|
|
187
301
|
...report.findings.map((finding) => ` ${finding.severity.toUpperCase()} ${escapeTerminal(finding.id)}: ${escapeTerminal(finding.message)}`),
|
|
188
|
-
...report.warnings.map((warning) => ` WARNING evidence: ${escapeTerminal(warning)}`)
|
|
302
|
+
...report.warnings.map((warning) => ` WARNING evidence: ${escapeTerminal(warning)}`),
|
|
189
303
|
].join("\n");
|
|
190
304
|
}
|
|
191
305
|
export function compareEvidenceReports(left, right) {
|
|
@@ -200,21 +314,34 @@ export function compareEvidenceReports(left, right) {
|
|
|
200
314
|
differences.push("egress.productionAttempts differs");
|
|
201
315
|
if (left.secrets.matches !== right.secrets.matches)
|
|
202
316
|
differences.push("secrets.matches differs");
|
|
203
|
-
if (stableStringify(left.contractDrift ?? emptyContractDrift()) !==
|
|
317
|
+
if (stableStringify(left.contractDrift ?? emptyContractDrift()) !==
|
|
318
|
+
stableStringify(right.contractDrift ?? emptyContractDrift()))
|
|
204
319
|
differences.push("contractDrift differs");
|
|
205
320
|
if (stableStringify(left.coverage) !== stableStringify(right.coverage))
|
|
206
321
|
differences.push("coverage differs");
|
|
207
322
|
if (stableStringify(left.findings) !== stableStringify(right.findings))
|
|
208
323
|
differences.push("findings differ");
|
|
209
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
equal: differences.length === 0,
|
|
326
|
+
leftHash: left.artifact.logicalHash,
|
|
327
|
+
rightHash: right.artifact.logicalHash,
|
|
328
|
+
differences,
|
|
329
|
+
};
|
|
210
330
|
}
|
|
211
331
|
export function formatEvidenceCompare(result) {
|
|
212
332
|
if (result.equal)
|
|
213
333
|
return `Evidence reports match: ${result.leftHash}`;
|
|
214
|
-
return [
|
|
334
|
+
return [
|
|
335
|
+
`Evidence reports differ:`,
|
|
336
|
+
`Left: ${result.leftHash}`,
|
|
337
|
+
`Right: ${result.rightHash}`,
|
|
338
|
+
...result.differences.map((difference) => ` - ${escapeTerminal(difference)}`),
|
|
339
|
+
].join("\n");
|
|
215
340
|
}
|
|
216
341
|
async function writeEvidenceReport(report, projectRoot, outPath) {
|
|
217
|
-
const target = outPath === undefined
|
|
342
|
+
const target = outPath === undefined
|
|
343
|
+
? join(getDataPaths().reports, `${report.run.id === "unknown" ? "latest" : report.run.id}.json`)
|
|
344
|
+
: await resolveOutputPath(outPath, projectRoot);
|
|
218
345
|
await ensurePrivateDirectory(dirname(target));
|
|
219
346
|
await atomicWriteJson(target, report);
|
|
220
347
|
const source = await readFile(target, "utf8");
|
|
@@ -260,10 +387,12 @@ async function readPersistedEvents(runEvidencePath) {
|
|
|
260
387
|
return events;
|
|
261
388
|
}
|
|
262
389
|
async function readRunEvidence(runPath, projectRoot) {
|
|
263
|
-
const path = runPath ?? await findLatestRunEvidence();
|
|
390
|
+
const path = runPath ?? (await findLatestRunEvidence());
|
|
264
391
|
if (path === undefined)
|
|
265
392
|
return null;
|
|
266
|
-
const resolved = runPath === undefined
|
|
393
|
+
const resolved = runPath === undefined
|
|
394
|
+
? path
|
|
395
|
+
: await resolveExistingReportPath(runPath, projectRoot);
|
|
267
396
|
const source = await readFile(resolved, "utf8");
|
|
268
397
|
if (Buffer.byteLength(source, "utf8") > MAX_RUN_EVIDENCE_BYTES)
|
|
269
398
|
throw new EvidenceReportError(`Run evidence exceeds ${MAX_RUN_EVIDENCE_BYTES} bytes.`);
|
|
@@ -280,11 +409,16 @@ async function loadContractDiff(baselinePath, candidatePath, projectRoot) {
|
|
|
280
409
|
if (baselinePath === undefined || candidatePath === undefined)
|
|
281
410
|
throw new EvidenceReportError("Contract drift evidence requires both --contract-baseline and --contract-candidate.");
|
|
282
411
|
try {
|
|
283
|
-
const [baseline, candidate] = await Promise.all([
|
|
412
|
+
const [baseline, candidate] = await Promise.all([
|
|
413
|
+
loadContract(baselinePath, projectRoot),
|
|
414
|
+
loadContract(candidatePath, projectRoot),
|
|
415
|
+
]);
|
|
284
416
|
return diffContracts(baseline, candidate);
|
|
285
417
|
}
|
|
286
418
|
catch (error) {
|
|
287
|
-
throw new EvidenceReportError(error instanceof Error
|
|
419
|
+
throw new EvidenceReportError(error instanceof Error
|
|
420
|
+
? `Contract drift input is invalid: ${error.message}`
|
|
421
|
+
: "Contract drift input is invalid.");
|
|
288
422
|
}
|
|
289
423
|
}
|
|
290
424
|
async function findLatestRunEvidence() {
|
|
@@ -300,7 +434,8 @@ async function findLatestRunEvidence() {
|
|
|
300
434
|
if (info?.isFile())
|
|
301
435
|
candidates.push({ path: evidencePath, mtimeMs: info.mtimeMs });
|
|
302
436
|
}
|
|
303
|
-
return candidates.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]
|
|
437
|
+
return candidates.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]
|
|
438
|
+
?.path;
|
|
304
439
|
}
|
|
305
440
|
catch (error) {
|
|
306
441
|
if (isErrorCode(error, "ENOENT"))
|
|
@@ -310,7 +445,9 @@ async function findLatestRunEvidence() {
|
|
|
310
445
|
}
|
|
311
446
|
async function pruneReports() {
|
|
312
447
|
try {
|
|
313
|
-
const entries = await readdir(getDataPaths().reports, {
|
|
448
|
+
const entries = await readdir(getDataPaths().reports, {
|
|
449
|
+
withFileTypes: true,
|
|
450
|
+
});
|
|
314
451
|
const reports = [];
|
|
315
452
|
for (const entry of entries) {
|
|
316
453
|
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
@@ -320,7 +457,9 @@ async function pruneReports() {
|
|
|
320
457
|
if (!info.isSymbolicLink())
|
|
321
458
|
reports.push({ path, mtimeMs: info.mtimeMs });
|
|
322
459
|
}
|
|
323
|
-
for (const report of reports
|
|
460
|
+
for (const report of reports
|
|
461
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
462
|
+
.slice(MAX_REPORTS_RETAINED))
|
|
324
463
|
await rm(report.path, { force: true });
|
|
325
464
|
}
|
|
326
465
|
catch (error) {
|
|
@@ -329,7 +468,9 @@ async function pruneReports() {
|
|
|
329
468
|
}
|
|
330
469
|
}
|
|
331
470
|
async function resolveOutputPath(outPath, projectRoot) {
|
|
332
|
-
const target = isAbsolute(outPath)
|
|
471
|
+
const target = isAbsolute(outPath)
|
|
472
|
+
? resolve(outPath)
|
|
473
|
+
: resolve(projectRoot, outPath);
|
|
333
474
|
assertPathInsideAllowedRoots(projectRoot, target);
|
|
334
475
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
335
476
|
const parent = await lstat(dirname(target));
|
|
@@ -354,17 +495,27 @@ function normalizeEvidenceReport(value) {
|
|
|
354
495
|
const report = value;
|
|
355
496
|
if (report.schemaVersion !== 1)
|
|
356
497
|
throw new EvidenceReportError("Unsupported evidence report schema version.");
|
|
357
|
-
if (typeof report.artifact?.logicalHash !== "string" ||
|
|
498
|
+
if (typeof report.artifact?.logicalHash !== "string" ||
|
|
499
|
+
!/^[a-f0-9]{64}$/.test(report.artifact.logicalHash))
|
|
358
500
|
throw new EvidenceReportError("Evidence report is missing a valid logical hash.");
|
|
359
501
|
if (report.artifact.canonicalization !== "json-stable-sorted-keys-v1")
|
|
360
502
|
throw new EvidenceReportError("Unsupported evidence report canonicalization.");
|
|
361
|
-
if (typeof report.run?.id !== "string" ||
|
|
503
|
+
if (typeof report.run?.id !== "string" ||
|
|
504
|
+
typeof report.summary?.passed !== "boolean" ||
|
|
505
|
+
!Array.isArray(report.findings))
|
|
362
506
|
throw new EvidenceReportError("Evidence report has an invalid schema.");
|
|
363
507
|
return report;
|
|
364
508
|
}
|
|
365
509
|
function sanitizeEvent(event) {
|
|
366
510
|
const sanitized = sanitizeSecrets(event);
|
|
367
|
-
return {
|
|
511
|
+
return {
|
|
512
|
+
...sanitized,
|
|
513
|
+
provider: limitText(sanitized.provider),
|
|
514
|
+
method: limitText(sanitized.method),
|
|
515
|
+
path: sanitizePath(sanitized.path),
|
|
516
|
+
request: sanitizeSecrets(sanitized.request),
|
|
517
|
+
response: sanitizeSecrets(sanitized.response),
|
|
518
|
+
};
|
|
368
519
|
}
|
|
369
520
|
function sanitizePath(path) {
|
|
370
521
|
const sanitized = sanitizeSecretString(path);
|
|
@@ -373,7 +524,9 @@ function sanitizePath(path) {
|
|
|
373
524
|
return limitText(pathname ?? "");
|
|
374
525
|
const params = query.split("&").map((part) => {
|
|
375
526
|
const [key = "", value = ""] = part.split("=", 2);
|
|
376
|
-
return SENSITIVE_QUERY_KEYS.has(decodeURIComponentSafe(key).toLowerCase())
|
|
527
|
+
return SENSITIVE_QUERY_KEYS.has(decodeURIComponentSafe(key).toLowerCase())
|
|
528
|
+
? `${key}=***`
|
|
529
|
+
: `${key}=${sanitizeSecretString(value)}`;
|
|
377
530
|
});
|
|
378
531
|
return limitText(`${pathname}?${params.join("&")}`);
|
|
379
532
|
}
|
|
@@ -393,7 +546,9 @@ function collectScenarioStrings(value, scenarios) {
|
|
|
393
546
|
}
|
|
394
547
|
if (value !== null && typeof value === "object") {
|
|
395
548
|
for (const [key, entry] of Object.entries(value)) {
|
|
396
|
-
if (key.toLowerCase() === "scenario" &&
|
|
549
|
+
if (key.toLowerCase() === "scenario" &&
|
|
550
|
+
typeof entry === "string" &&
|
|
551
|
+
/^[a-z0-9][a-z0-9._-]{0,127}$/.test(entry))
|
|
397
552
|
scenarios.add(entry);
|
|
398
553
|
collectScenarioStrings(entry, scenarios);
|
|
399
554
|
}
|
|
@@ -449,7 +604,9 @@ function hashLogicalReport(report) {
|
|
|
449
604
|
const logical = structuredClone(report);
|
|
450
605
|
logical.artifact.logicalHash = "";
|
|
451
606
|
logical.artifact.generatedAt = "";
|
|
452
|
-
return createHash("sha256")
|
|
607
|
+
return createHash("sha256")
|
|
608
|
+
.update(stableStringify(logical), "utf8")
|
|
609
|
+
.digest("hex");
|
|
453
610
|
}
|
|
454
611
|
function stableStringify(value) {
|
|
455
612
|
if (value === undefined)
|
|
@@ -458,32 +615,58 @@ function stableStringify(value) {
|
|
|
458
615
|
return JSON.stringify(value);
|
|
459
616
|
if (Array.isArray(value))
|
|
460
617
|
return `[${value.map(stableStringify).join(",")}]`;
|
|
461
|
-
const entries = Object.entries(value)
|
|
618
|
+
const entries = Object.entries(value)
|
|
619
|
+
.filter(([, entry]) => entry !== undefined)
|
|
620
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
462
621
|
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
|
|
463
622
|
}
|
|
464
623
|
function firstTimestamp(events) {
|
|
465
|
-
return events
|
|
624
|
+
return events
|
|
625
|
+
.map((event) => readString(event.timestamp))
|
|
626
|
+
.filter((value) => value !== undefined)
|
|
627
|
+
.sort()[0];
|
|
466
628
|
}
|
|
467
629
|
function lastTimestamp(events) {
|
|
468
|
-
return events
|
|
630
|
+
return events
|
|
631
|
+
.map((event) => readString(event.timestamp))
|
|
632
|
+
.filter((value) => value !== undefined)
|
|
633
|
+
.sort()
|
|
634
|
+
.at(-1);
|
|
469
635
|
}
|
|
470
636
|
function lastExitCode(events) {
|
|
471
|
-
return events
|
|
637
|
+
return events
|
|
638
|
+
.map((event) => event.exitCode)
|
|
639
|
+
.filter((value) => Number.isInteger(value))
|
|
640
|
+
.at(-1);
|
|
472
641
|
}
|
|
473
642
|
function hasRetryAfter(event) {
|
|
474
|
-
return JSON.stringify(event.response).toLowerCase().includes("retry-after") ||
|
|
643
|
+
return (JSON.stringify(event.response).toLowerCase().includes("retry-after") ||
|
|
644
|
+
JSON.stringify(event.request).toLowerCase().includes("retry-after"));
|
|
475
645
|
}
|
|
476
646
|
function readRunStatus(value) {
|
|
477
|
-
return value === "preparing" ||
|
|
647
|
+
return value === "preparing" ||
|
|
648
|
+
value === "running" ||
|
|
649
|
+
value === "failed-to-start" ||
|
|
650
|
+
value === "finished"
|
|
651
|
+
? value
|
|
652
|
+
: "unknown";
|
|
478
653
|
}
|
|
479
654
|
function readString(value) {
|
|
480
|
-
return typeof value === "string" && value.trim() !== ""
|
|
655
|
+
return typeof value === "string" && value.trim() !== ""
|
|
656
|
+
? limitText(sanitizeSecretString(value))
|
|
657
|
+
: undefined;
|
|
481
658
|
}
|
|
482
659
|
function readStringArray(value) {
|
|
483
|
-
return Array.isArray(value)
|
|
660
|
+
return Array.isArray(value)
|
|
661
|
+
? uniqueSorted(value
|
|
662
|
+
.filter((entry) => typeof entry === "string")
|
|
663
|
+
.map(limitText))
|
|
664
|
+
: [];
|
|
484
665
|
}
|
|
485
666
|
function readNonNegativeInteger(value) {
|
|
486
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
667
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
668
|
+
? value
|
|
669
|
+
: undefined;
|
|
487
670
|
}
|
|
488
671
|
function readBoolean(value) {
|
|
489
672
|
return value === true;
|
|
@@ -496,10 +679,14 @@ function emptyContractDrift() {
|
|
|
496
679
|
}
|
|
497
680
|
function limitText(value) {
|
|
498
681
|
const sanitized = sanitizeSecretString(value).replace(/[\r\n\t]/g, " ");
|
|
499
|
-
return sanitized.length > 2_000
|
|
682
|
+
return sanitized.length > 2_000
|
|
683
|
+
? `${sanitized.slice(0, 2_000)}[truncated]`
|
|
684
|
+
: sanitized;
|
|
500
685
|
}
|
|
501
686
|
function escapeTerminal(value) {
|
|
502
|
-
return value
|
|
687
|
+
return value
|
|
688
|
+
.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "")
|
|
689
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
503
690
|
}
|
|
504
691
|
function secretCategory(key) {
|
|
505
692
|
const normalized = key.toLowerCase();
|
|
@@ -516,7 +703,9 @@ function secretCategory(key) {
|
|
|
516
703
|
return "secret-field";
|
|
517
704
|
}
|
|
518
705
|
function readPackageVersion() {
|
|
519
|
-
return typeof packageJson.version === "string"
|
|
706
|
+
return typeof packageJson.version === "string"
|
|
707
|
+
? packageJson.version
|
|
708
|
+
: "unknown";
|
|
520
709
|
}
|
|
521
710
|
async function realProjectRoot(projectRoot) {
|
|
522
711
|
const root = resolve(projectRoot);
|
|
@@ -525,13 +714,6 @@ async function realProjectRoot(projectRoot) {
|
|
|
525
714
|
throw new EvidenceReportError("Project root must be a real directory, not a symlink.");
|
|
526
715
|
return root;
|
|
527
716
|
}
|
|
528
|
-
function assertPathInside(root, target) {
|
|
529
|
-
const relativePath = relative(root, target);
|
|
530
|
-
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath))
|
|
531
|
-
throw new EvidenceReportError("Evidence path traversal outside the project root is not allowed.");
|
|
532
|
-
if (basename(target).trim() === "")
|
|
533
|
-
throw new EvidenceReportError("Evidence path must include a file name.");
|
|
534
|
-
}
|
|
535
717
|
function assertPathInsideAllowedRoots(projectRoot, target) {
|
|
536
718
|
if (!isInside(projectRoot, target) && !isInside(getDataPaths().root, target))
|
|
537
719
|
throw new EvidenceReportError("Evidence path traversal outside the project root or GHOSTAPI_DATA_DIR is not allowed.");
|
|
@@ -540,7 +722,9 @@ function assertPathInsideAllowedRoots(projectRoot, target) {
|
|
|
540
722
|
}
|
|
541
723
|
function isInside(root, target) {
|
|
542
724
|
const relativePath = relative(resolve(root), target);
|
|
543
|
-
return relativePath !== ".." &&
|
|
725
|
+
return (relativePath !== ".." &&
|
|
726
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
727
|
+
!isAbsolute(relativePath));
|
|
544
728
|
}
|
|
545
729
|
function decodeURIComponentSafe(value) {
|
|
546
730
|
try {
|