avorelo 0.3.8 → 0.4.0-rc.2
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/LICENSE +16 -23
- package/README.md +62 -72
- package/bin/avorelo +9 -0
- package/package.json +147 -139
- package/scripts/README.md +40 -0
- package/scripts/avorelo-session-collision-guard.js +540 -0
- package/scripts/avorelo-workstream-preflight.js +918 -0
- package/scripts/cco-dashboard.js +346 -0
- package/scripts/cco-posttooluse.js +504 -0
- package/scripts/cco-pretooluse.js +381 -0
- package/scripts/cco-status.js +540 -0
- package/scripts/cco-stop.js +77 -0
- package/scripts/cco-userprompt-submit.js +83 -0
- package/scripts/lib/activation/account-state.js +37 -0
- package/scripts/lib/activation/activate-command.js +114 -0
- package/scripts/lib/activation/activation-runner.js +607 -0
- package/scripts/lib/activation/activation-self-healing.js +481 -0
- package/scripts/lib/activation/activation-state.js +96 -0
- package/scripts/lib/activation/activation-summary.js +241 -0
- package/scripts/lib/activation/adapters/claude-code.js +77 -0
- package/scripts/lib/activation/adapters/codex-cli.js +52 -0
- package/scripts/lib/activation/adapters/cursor.js +37 -0
- package/scripts/lib/activation/adapters/github-agent.js +39 -0
- package/scripts/lib/activation/adapters/terminal.js +42 -0
- package/scripts/lib/activation/adapters/vscode.js +39 -0
- package/scripts/lib/activation/adapters/windsurf.js +37 -0
- package/scripts/lib/activation/ai-surface-detector.js +151 -0
- package/scripts/lib/activation/connect-account.js +145 -0
- package/scripts/lib/activation/detect-environment.js +75 -0
- package/scripts/lib/activation/detect-hosts.js +62 -0
- package/scripts/lib/activation/format-activation-output.js +109 -0
- package/scripts/lib/activation/next-action.js +43 -0
- package/scripts/lib/activation/repair-engine.js +219 -0
- package/scripts/lib/activation/run-entry.js +346 -0
- package/scripts/lib/activation-distribution-readiness.js +507 -0
- package/scripts/lib/adapter-conformance.js +176 -0
- package/scripts/lib/adapter-readiness.js +428 -0
- package/scripts/lib/adapter-safety-boundaries.js +335 -0
- package/scripts/lib/adapter-technical-readiness-gate.js +205 -0
- package/scripts/lib/agent-access-governance.js +455 -0
- package/scripts/lib/agent-activation-readiness.js +878 -0
- package/scripts/lib/agent-enforcement.js +765 -0
- package/scripts/lib/agent-evolution-loop.js +479 -0
- package/scripts/lib/agent-policy-profile.js +210 -0
- package/scripts/lib/agent-security/action-evaluator.js +507 -0
- package/scripts/lib/agent-security/adapter-registry.js +98 -0
- package/scripts/lib/agent-security/auto-policy.js +139 -0
- package/scripts/lib/agent-security/bounded-scan.js +93 -0
- package/scripts/lib/agent-security/enforcement-adapter.js +174 -0
- package/scripts/lib/agent-security/enforcement-engine.js +1129 -0
- package/scripts/lib/agent-security/file-write-adapter.js +183 -0
- package/scripts/lib/agent-security/file-write-rules.js +178 -0
- package/scripts/lib/agent-security/index.js +3342 -0
- package/scripts/lib/agent-security/instruction-risk.js +181 -0
- package/scripts/lib/agent-security/mcp-action-adapter.js +185 -0
- package/scripts/lib/agent-security/mcp-action-rules.js +184 -0
- package/scripts/lib/agent-security/package-action-adapter.js +175 -0
- package/scripts/lib/agent-security/package-action-rules.js +233 -0
- package/scripts/lib/agent-security/performance.js +148 -0
- package/scripts/lib/agent-security/permission-minimizer.js +403 -0
- package/scripts/lib/agent-security/scan-cache.js +74 -0
- package/scripts/lib/agent-security/source-trust.js +146 -0
- package/scripts/lib/ai-efficiency-dogfood-pack.js +437 -0
- package/scripts/lib/ai-efficiency-eval.js +207 -0
- package/scripts/lib/ai-efficiency-guard-contract.js +209 -0
- package/scripts/lib/ai-efficiency-measured-value.js +106 -0
- package/scripts/lib/ai-install-prompt.js +293 -0
- package/scripts/lib/ai-task-inference.js +155 -0
- package/scripts/lib/ai-value-ledger.js +121 -0
- package/scripts/lib/ai-value-proof-table.js +151 -0
- package/scripts/lib/ai-work-efficiency-routing.js +623 -0
- package/scripts/lib/ai-workspace-hygiene.js +1499 -0
- package/scripts/lib/alpha-activation.js +520 -0
- package/scripts/lib/alpha-feedback.js +263 -0
- package/scripts/lib/alpha-readiness-gate.js +332 -0
- package/scripts/lib/anti-gaming.js +169 -0
- package/scripts/lib/artifact-health.js +431 -0
- package/scripts/lib/artifact-receipt-hygiene.js +619 -0
- package/scripts/lib/attribution.js +180 -0
- package/scripts/lib/audit.js +289 -0
- package/scripts/lib/avorelo-skill-registry.js +810 -0
- package/scripts/lib/batch-jobs.js +71 -0
- package/scripts/lib/brain-pack.js +578 -0
- package/scripts/lib/brand-boundary.js +424 -0
- package/scripts/lib/brand.js +74 -0
- package/scripts/lib/browser-capability.js +1048 -0
- package/scripts/lib/browser-proof-governed-workflow.js +1020 -0
- package/scripts/lib/browser-proof-preflight.js +321 -0
- package/scripts/lib/cache-readiness.js +296 -0
- package/scripts/lib/canonical-reentry.js +162 -0
- package/scripts/lib/capability-contract-validator.js +177 -0
- package/scripts/lib/capability-hygiene.js +504 -0
- package/scripts/lib/capability-intake-quarantine.js +789 -0
- package/scripts/lib/capability-kernel-registry.js +566 -0
- package/scripts/lib/capability-kernel.js +202 -0
- package/scripts/lib/capability-manifest.js +255 -0
- package/scripts/lib/capability-outcome-normalizer.js +104 -0
- package/scripts/lib/capability-packs.js +314 -0
- package/scripts/lib/capability-recommender.js +513 -0
- package/scripts/lib/capability-registry.js +1062 -0
- package/scripts/lib/carry-forward-surfacing.js +194 -0
- package/scripts/lib/ccusage-adapter.js +188 -0
- package/scripts/lib/claude-session-evidence/index.js +1027 -0
- package/scripts/lib/cli-output-sanitizer.js +118 -0
- package/scripts/lib/company-loop.js +1149 -0
- package/scripts/lib/config.js +641 -0
- package/scripts/lib/context-acquisition-plan.js +359 -0
- package/scripts/lib/context-authority-rules.js +138 -0
- package/scripts/lib/context-budget-guard.js +170 -0
- package/scripts/lib/context-budget-scanner.js +257 -0
- package/scripts/lib/context-budget.js +167 -0
- package/scripts/lib/context-claim-safety.js +116 -0
- package/scripts/lib/context-compiler.js +236 -0
- package/scripts/lib/context-optimizer.js +715 -0
- package/scripts/lib/context-packet.js +121 -0
- package/scripts/lib/context-receipt.js +57 -0
- package/scripts/lib/context-reduction-plan.js +179 -0
- package/scripts/lib/context-safety.js +88 -0
- package/scripts/lib/context-savings-engine.js +158 -0
- package/scripts/lib/context-source-registry.js +393 -0
- package/scripts/lib/context-task-profiles.js +147 -0
- package/scripts/lib/cost-abuse-guard.js +975 -0
- package/scripts/lib/cost-evidence.js +287 -0
- package/scripts/lib/cost-to-outcome-signals.js +639 -0
- package/scripts/lib/cross-host-install-plan.js +308 -0
- package/scripts/lib/cross-host-install-readiness.js +242 -0
- package/scripts/lib/cross-host-value-flow.js +268 -0
- package/scripts/lib/dashboard.js +1056 -0
- package/scripts/lib/data-permissions-guard.js +1187 -0
- package/scripts/lib/deployment-environment-guard.js +1236 -0
- package/scripts/lib/design-partner-feedback.js +346 -0
- package/scripts/lib/dogfood-evidence-freshness.js +567 -0
- package/scripts/lib/efficiency-receipt.js +354 -0
- package/scripts/lib/entitlements.js +100 -0
- package/scripts/lib/execution-packet.js +559 -0
- package/scripts/lib/execution-route-governor.js +179 -0
- package/scripts/lib/experimentation-events.js +547 -0
- package/scripts/lib/external-capability-compliance.js +107 -0
- package/scripts/lib/external-readiness/ai-visibility/ai-visibility-runner.js +322 -0
- package/scripts/lib/external-readiness/calibration/benchmark-site-catalog.js +121 -0
- package/scripts/lib/external-readiness/calibration/completion-gate.js +112 -0
- package/scripts/lib/external-readiness/calibration/real-site-benchmark-runner.js +284 -0
- package/scripts/lib/external-readiness/drift-detector.js +100 -0
- package/scripts/lib/external-readiness/endpoint-prober.js +332 -0
- package/scripts/lib/external-readiness/external-readiness-runner.js +112 -0
- package/scripts/lib/external-readiness/full-agent-readiness-scorer.js +220 -0
- package/scripts/lib/external-readiness/golden-path-runner.js +333 -0
- package/scripts/lib/external-readiness/inside-out-runner.js +118 -0
- package/scripts/lib/external-readiness/proof-receipt-writer.js +112 -0
- package/scripts/lib/external-readiness/safe-fix-executor.js +117 -0
- package/scripts/lib/external-readiness/skills/category-boundary-guard.js +30 -0
- package/scripts/lib/external-readiness/skills/pmm-positioning-guard.js +69 -0
- package/scripts/lib/external-readiness/skills/safe-fix-policy.js +35 -0
- package/scripts/lib/external-user-simulation.js +166 -0
- package/scripts/lib/failure-recovery-readiness.js +81 -0
- package/scripts/lib/failure-recovery.js +419 -0
- package/scripts/lib/feedback-intelligence.js +537 -0
- package/scripts/lib/feedback-signals.js +205 -0
- package/scripts/lib/file-integrity.js +68 -0
- package/scripts/lib/fsx.js +129 -0
- package/scripts/lib/full-readiness-gate.js +451 -0
- package/scripts/lib/governed-action-exposure-primitives.js +1514 -0
- package/scripts/lib/governed-exposure-planner.js +186 -0
- package/scripts/lib/governed-workspace-hygiene.js +946 -0
- package/scripts/lib/guard-suite-common.js +412 -0
- package/scripts/lib/guidance-builder.js +174 -0
- package/scripts/lib/hook-apply.js +1075 -0
- package/scripts/lib/hook-baseline.js +310 -0
- package/scripts/lib/hook-config-preview.js +275 -0
- package/scripts/lib/hook-contracts.js +290 -0
- package/scripts/lib/hook-safety-boundary-readiness.js +80 -0
- package/scripts/lib/host-capability-matrix.js +351 -0
- package/scripts/lib/host-support-context.js +254 -0
- package/scripts/lib/http-hook-action.js +538 -0
- package/scripts/lib/install-ai-readiness.js +84 -0
- package/scripts/lib/install-intake-risk.js +1051 -0
- package/scripts/lib/install-journey-intelligence.js +329 -0
- package/scripts/lib/intervention-guidance.js +57 -0
- package/scripts/lib/known-limitations.js +115 -0
- package/scripts/lib/l8-path-truth.js +146 -0
- package/scripts/lib/launch-hardening-gate.js +436 -0
- package/scripts/lib/launch-readiness.js +635 -0
- package/scripts/lib/learning-experimentation-foundation.js +1484 -0
- package/scripts/lib/learning-memory.js +686 -0
- package/scripts/lib/lifecycle-hooks.js +982 -0
- package/scripts/lib/local-package-smoke.js +436 -0
- package/scripts/lib/local-pricing.js +299 -0
- package/scripts/lib/mcp-enforcement.js +311 -0
- package/scripts/lib/mcp-least-privilege-policy.js +303 -0
- package/scripts/lib/mcp-tool-inventory.js +388 -0
- package/scripts/lib/mcp-tool-risk.js +0 -0
- package/scripts/lib/memory.js +335 -0
- package/scripts/lib/metrics.js +699 -0
- package/scripts/lib/micro-proof.js +133 -0
- package/scripts/lib/model-work-class-routing.js +79 -0
- package/scripts/lib/next-run-context.js +436 -0
- package/scripts/lib/next-run-continuity.js +1207 -0
- package/scripts/lib/observability-recovery-guard.js +1030 -0
- package/scripts/lib/operating-value.js +1662 -0
- package/scripts/lib/optimization-v3.js +122 -0
- package/scripts/lib/orchestration/adapters/_shared.js +49 -0
- package/scripts/lib/orchestration/adapters/aider.js +18 -0
- package/scripts/lib/orchestration/adapters/claude-code.js +35 -0
- package/scripts/lib/orchestration/adapters/codex.js +35 -0
- package/scripts/lib/orchestration/adapters/gemini-cli.js +18 -0
- package/scripts/lib/orchestration/adapters/git.js +25 -0
- package/scripts/lib/orchestration/adapters/index.js +31 -0
- package/scripts/lib/orchestration/adapters/lm-studio.js +18 -0
- package/scripts/lib/orchestration/adapters/ollama.js +18 -0
- package/scripts/lib/orchestration/adapters/opencode.js +18 -0
- package/scripts/lib/orchestration/adapters/openrouter.js +18 -0
- package/scripts/lib/orchestration/adapters/test-runner.js +25 -0
- package/scripts/lib/orchestration/cli.js +438 -0
- package/scripts/lib/orchestration/execution-manager.js +279 -0
- package/scripts/lib/orchestration/handoff.js +314 -0
- package/scripts/lib/orchestration/index.js +456 -0
- package/scripts/lib/orchestration/inventory.js +47 -0
- package/scripts/lib/orchestration/model-discovery.js +498 -0
- package/scripts/lib/orchestration/model-profiler.js +170 -0
- package/scripts/lib/orchestration/model-profiles.js +252 -0
- package/scripts/lib/orchestration/model-refresh-policy.js +72 -0
- package/scripts/lib/orchestration/proof-writer.js +349 -0
- package/scripts/lib/orchestration/provider-discovery/aider.js +49 -0
- package/scripts/lib/orchestration/provider-discovery/claude-code.js +56 -0
- package/scripts/lib/orchestration/provider-discovery/codex.js +49 -0
- package/scripts/lib/orchestration/provider-discovery/common.js +186 -0
- package/scripts/lib/orchestration/provider-discovery/gemini.js +106 -0
- package/scripts/lib/orchestration/provider-discovery/lm-studio.js +118 -0
- package/scripts/lib/orchestration/provider-discovery/models-dev.js +12 -0
- package/scripts/lib/orchestration/provider-discovery/ollama.js +100 -0
- package/scripts/lib/orchestration/provider-discovery/opencode.js +47 -0
- package/scripts/lib/orchestration/provider-discovery/openrouter.js +44 -0
- package/scripts/lib/orchestration/risk-classifier.js +130 -0
- package/scripts/lib/orchestration/routing-policy.js +486 -0
- package/scripts/lib/orchestration/settings.js +95 -0
- package/scripts/lib/orchestration/state.js +165 -0
- package/scripts/lib/orchestration/verification-manager.js +138 -0
- package/scripts/lib/outcome-evidence-stack.js +1083 -0
- package/scripts/lib/output-profiles.js +146 -0
- package/scripts/lib/package-content-audit.js +368 -0
- package/scripts/lib/package-runtime.js +278 -0
- package/scripts/lib/plan-surface.js +53 -0
- package/scripts/lib/plans.js +2327 -0
- package/scripts/lib/policy-provider.js +27 -0
- package/scripts/lib/prelaunch-activation-readiness.js +409 -0
- package/scripts/lib/prelaunch-evidence-store.js +816 -0
- package/scripts/lib/prelaunch-intelligence.js +869 -0
- package/scripts/lib/pricing-experiment.js +118 -0
- package/scripts/lib/pro-moment-events.js +77 -0
- package/scripts/lib/pro-moment-state.js +227 -0
- package/scripts/lib/pro-moments.js +1216 -0
- package/scripts/lib/product-home-receipt.js +1790 -0
- package/scripts/lib/product-learning-events.js +696 -0
- package/scripts/lib/production-confidence-control-tower.js +1209 -0
- package/scripts/lib/production-confidence.js +1858 -0
- package/scripts/lib/project-profile.js +555 -0
- package/scripts/lib/prompt-compiler-plan.js +94 -0
- package/scripts/lib/prompt-compiler.js +280 -0
- package/scripts/lib/prompt-lint.js +32 -0
- package/scripts/lib/prompt-suggestions.js +52 -0
- package/scripts/lib/proof-canonical.js +398 -0
- package/scripts/lib/proof-drilldown.js +383 -0
- package/scripts/lib/proof-e2e/promise-completion.js +141 -0
- package/scripts/lib/proof-e2e/promise-dashboard-claims.js +135 -0
- package/scripts/lib/proof-e2e/promise-local-proof.js +156 -0
- package/scripts/lib/proof-e2e/promise-provider-neutral.js +136 -0
- package/scripts/lib/proof-e2e/promise-registry.js +219 -0
- package/scripts/lib/proof-e2e/promise-security-trust.js +141 -0
- package/scripts/lib/proof-e2e/promise-skills-risk.js +163 -0
- package/scripts/lib/proof-e2e/promise-token-savings.js +112 -0
- package/scripts/lib/proof-e2e/promise-work-management.js +126 -0
- package/scripts/lib/proof-events.js +342 -0
- package/scripts/lib/proof-history.js +243 -0
- package/scripts/lib/proof-metrics.js +296 -0
- package/scripts/lib/proof-outcome-evidence.js +134 -0
- package/scripts/lib/proof-receipt.js +335 -0
- package/scripts/lib/proof-record.js +461 -0
- package/scripts/lib/proof-savings-report.js +1549 -0
- package/scripts/lib/public-activation-distribution-gate.js +266 -0
- package/scripts/lib/public-cli.js +5631 -0
- package/scripts/lib/public-distribution-truth.js +211 -0
- package/scripts/lib/public-distribution.js +73 -0
- package/scripts/lib/public-install-claim-checker.js +300 -0
- package/scripts/lib/publish-provenance-readiness.js +287 -0
- package/scripts/lib/readiness-delta.js +218 -0
- package/scripts/lib/readiness-evidence-closure.js +196 -0
- package/scripts/lib/reentry-memory-capture.js +241 -0
- package/scripts/lib/reentry-memory-retrieval.js +302 -0
- package/scripts/lib/reentry-memory-status.js +147 -0
- package/scripts/lib/reentry-memory-store.js +183 -0
- package/scripts/lib/reentry-state.js +66 -0
- package/scripts/lib/release-candidate-bundle.js +166 -0
- package/scripts/lib/remediation.js +81 -0
- package/scripts/lib/repo-map.js +391 -0
- package/scripts/lib/run-action-policy.js +104 -0
- package/scripts/lib/run-contract-receipt.js +48 -0
- package/scripts/lib/run-contract.js +288 -0
- package/scripts/lib/run-cost-to-verified-outcome.js +87 -0
- package/scripts/lib/run-improvements-lifecycle.js +330 -0
- package/scripts/lib/run-improvements.js +789 -0
- package/scripts/lib/run-proof-contract.js +116 -0
- package/scripts/lib/run-risk-classifier.js +131 -0
- package/scripts/lib/run-task-classifier.js +216 -0
- package/scripts/lib/runtime-decision-policy.js +387 -0
- package/scripts/lib/safe-path-engine.js +705 -0
- package/scripts/lib/safe-repair.js +877 -0
- package/scripts/lib/safe-run-controller.js +887 -0
- package/scripts/lib/scope-repair-before-approval.js +865 -0
- package/scripts/lib/score.js +262 -0
- package/scripts/lib/seamless-enforcement.js +329 -0
- package/scripts/lib/seamless-outcome.js +689 -0
- package/scripts/lib/seamless-reality-gate.js +5053 -0
- package/scripts/lib/security-confidence/activation-safety.js +78 -0
- package/scripts/lib/security-confidence/adapters/adapter-contract.js +125 -0
- package/scripts/lib/security-confidence/adapters/mock-claude-bughunter-adapter.js +36 -0
- package/scripts/lib/security-confidence/adapters/mock-decepticon-adapter.js +34 -0
- package/scripts/lib/security-confidence/adapters/mock-mcp-scan-adapter.js +35 -0
- package/scripts/lib/security-confidence/adapters/mock-snyk-agent-scan-adapter.js +35 -0
- package/scripts/lib/security-confidence/adapters/mock-tank-adapter.js +34 -0
- package/scripts/lib/security-confidence/adapters/reference-registry.js +63 -0
- package/scripts/lib/security-confidence/auth-rbac-plan.js +39 -0
- package/scripts/lib/security-confidence/component-inventory.js +151 -0
- package/scripts/lib/security-confidence/constants.js +191 -0
- package/scripts/lib/security-confidence/dependency-script-risk.js +95 -0
- package/scripts/lib/security-confidence/evidence-hygiene.js +127 -0
- package/scripts/lib/security-confidence/feature-impact-classifier.js +80 -0
- package/scripts/lib/security-confidence/file-utils.js +161 -0
- package/scripts/lib/security-confidence/finding-triage.js +90 -0
- package/scripts/lib/security-confidence/findings.js +115 -0
- package/scripts/lib/security-confidence/index.js +29 -0
- package/scripts/lib/security-confidence/permission-budget.js +93 -0
- package/scripts/lib/security-confidence/redaction.js +82 -0
- package/scripts/lib/security-confidence/redteam-fixtures.js +53 -0
- package/scripts/lib/security-confidence/reference-adoption.js +229 -0
- package/scripts/lib/security-confidence/reference-taxonomy.js +155 -0
- package/scripts/lib/security-confidence/report-quality.js +99 -0
- package/scripts/lib/security-confidence/report-writer.js +79 -0
- package/scripts/lib/security-confidence/risk-classifier.js +187 -0
- package/scripts/lib/security-confidence/runner.js +179 -0
- package/scripts/lib/security-confidence/schema.js +188 -0
- package/scripts/lib/security-confidence/secret-safety.js +74 -0
- package/scripts/lib/security-confidence/security-capability-router.js +98 -0
- package/scripts/lib/security-confidence/web-security-smoke-plan.js +46 -0
- package/scripts/lib/security-risk-classifier.js +511 -0
- package/scripts/lib/security-scan.js +396 -0
- package/scripts/lib/session-context-optimizer.js +1211 -0
- package/scripts/lib/session-timing.js +322 -0
- package/scripts/lib/skill-hygiene.js +805 -0
- package/scripts/lib/skill-packs.js +161 -0
- package/scripts/lib/skills-operating-layer-v1.js +1023 -0
- package/scripts/lib/skills-operating-layer.js +629 -0
- package/scripts/lib/smallest-safe-plan.js +214 -0
- package/scripts/lib/smart-routing-plan.js +115 -0
- package/scripts/lib/smart-work-routing.js +768 -0
- package/scripts/lib/source-catalog.js +1022 -0
- package/scripts/lib/status-value-summary.js +32 -0
- package/scripts/lib/supply-chain-dependency-guard.js +1333 -0
- package/scripts/lib/supply-chain-guard.js +219 -0
- package/scripts/lib/support-bundle.js +561 -0
- package/scripts/lib/support-redaction.js +66 -0
- package/scripts/lib/task-continuation.js +440 -0
- package/scripts/lib/test-helpers.js +15 -0
- package/scripts/lib/tier.js +38 -0
- package/scripts/lib/token-context-quality-gate.js +370 -0
- package/scripts/lib/token-cost-capture.js +187 -0
- package/scripts/lib/token-cost-evidence.js +862 -0
- package/scripts/lib/token-cost-intelligence.js +358 -0
- package/scripts/lib/token-efficiency-evidence.js +213 -0
- package/scripts/lib/token-efficiency-map.js +110 -0
- package/scripts/lib/token-evidence.js +703 -0
- package/scripts/lib/tokenish.js +17 -0
- package/scripts/lib/tool-output-sandbox.js +559 -0
- package/scripts/lib/trust-audit.js +136 -0
- package/scripts/lib/unified-events.js +396 -0
- package/scripts/lib/upgrade-interruption-recovery.js +407 -0
- package/scripts/lib/usage-ledger.js +201 -0
- package/scripts/lib/value-ledger.js +130 -0
- package/scripts/lib/value-proof-calibration.js +531 -0
- package/scripts/lib/visual-qa.js +231 -0
- package/scripts/lib/voice-alpha.js +29 -0
- package/scripts/lib/webhook-idempotency-guard.js +990 -0
- package/scripts/lib/work-aware-orchestration.js +976 -0
- package/scripts/lib/work-control-receipts.js +577 -0
- package/scripts/lib/work-ledger.js +1123 -0
- package/scripts/lib/work-panel-preview.js +352 -0
- package/scripts/lib/workflow-discipline.js +280 -0
- package/scripts/lib/workflow-intelligence-radar.js +804 -0
- package/scripts/lib/workflow-signals.js +419 -0
- package/scripts/lib/workspace-map.js +281 -0
- package/scripts/lib/workspace-registry.js +1367 -0
- package/scripts/lib/workspace-resolver.js +542 -0
- package/scripts/wuz-doctor.js +315 -0
- package/bin/avorelo.mjs +0 -7
- package/dist/avorelo.mjs +0 -24678
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const { ensureCcoDirs, nowIso, safeReadJson, safeWriteJson } = require("./fsx");
|
|
7
|
+
|
|
8
|
+
const CONTRACT = "avorelo.dataPermissionsGuard.v1";
|
|
9
|
+
const REPORT_DIR_REL = ".claude/cco/reports/data-permissions-guard";
|
|
10
|
+
const REPORT_JSON_REL = `${REPORT_DIR_REL}/latest-report.json`;
|
|
11
|
+
|
|
12
|
+
const TEXT_FILE_RE = /\.(cjs|cts|env|ini|js|json|jsx|md|mjs|mts|prisma|sql|toml|ts|tsx|txt|yaml|yml)$/i;
|
|
13
|
+
const SQL_FILE_RE = /\.sql$/i;
|
|
14
|
+
const ENV_FILE_RE = /(^|\/)\.env(?:\.[^/]+)?$|(^|\/)\.env\.example$|(^|\/)env\.example$/i;
|
|
15
|
+
const PRISMA_FILE_RE = /(^|\/)schema\.prisma$/i;
|
|
16
|
+
const DRIZZLE_CONFIG_RE = /(^|\/)drizzle\.config\.(cjs|js|json|mjs|mts|ts)$/i;
|
|
17
|
+
const SKIP_DIRS = new Set([
|
|
18
|
+
".claude",
|
|
19
|
+
".git",
|
|
20
|
+
".next",
|
|
21
|
+
".nuxt",
|
|
22
|
+
".turbo",
|
|
23
|
+
".wasp",
|
|
24
|
+
"build",
|
|
25
|
+
"coverage",
|
|
26
|
+
"dist",
|
|
27
|
+
"node_modules",
|
|
28
|
+
]);
|
|
29
|
+
const SENSITIVE_NAME_RE = /\b(user|account|session|tenant|organization|org|customer|invoice|payment|order|profile|document|contract|medical|patient|admin)\b/i;
|
|
30
|
+
const SENSITIVE_BUCKET_RE = /\b(invoice|invoices|billing|contract|contracts|document|documents|doc|docs|medical|patient|patients|private|upload|uploads|statement|statements)\b/i;
|
|
31
|
+
const LOWER_RISK_BUCKET_RE = /\b(avatar|avatars|image|images|photo|photos|public-assets)\b/i;
|
|
32
|
+
const CLIENT_PATH_RE = /(^|\/)(app\/(?!api\/)|browser\/|client\/|components\/|pages\/(?!api\/)|public\/|src\/client\/)/i;
|
|
33
|
+
const SERVER_PATH_RE = /(^|\/)(app\/api\/|pages\/api\/|server\/|src\/server\/|middleware\.(cjs|cts|js|mjs|mts|ts))|(^|\/)route\.(js|jsx|ts|tsx)$/i;
|
|
34
|
+
const API_ROUTE_RE = /(^|\/)(app\/api\/.*\/route\.(js|jsx|ts|tsx)|pages\/api\/.*\.(js|jsx|ts|tsx)|server\/routes\/.*\.(js|jsx|ts|tsx))$/i;
|
|
35
|
+
|
|
36
|
+
function unique(values) {
|
|
37
|
+
return [...new Set((values || []).filter(Boolean))];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toPosix(value) {
|
|
41
|
+
return String(value || "").replace(/\\/g, "/");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sanitizeText(value, max = 220) {
|
|
45
|
+
const text = String(value || "")
|
|
46
|
+
.replace(/\s+/g, " ")
|
|
47
|
+
.replace(/(?:sk-[A-Za-z0-9_-]{16,}|gh[ps]_[A-Za-z0-9]{12,}|npm_[A-Za-z0-9]{12,}|AKIA[0-9A-Z]{16})/g, "[redacted-token]")
|
|
48
|
+
.replace(/(bearer\s+)[A-Za-z0-9._-]{8,}/ig, "$1[redacted]")
|
|
49
|
+
.replace(/((?:api[_ -]?key|token|password|secret|authorization)\s*[:=]\s*)([^\s'"]+)/ig, "$1[redacted]")
|
|
50
|
+
.trim();
|
|
51
|
+
if (!text) return null;
|
|
52
|
+
return text.length > max ? `${text.slice(0, max - 3).trimEnd()}...` : text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sanitizeRef(ref) {
|
|
56
|
+
const text = String(ref || "").trim().replace(/\\/g, "/");
|
|
57
|
+
if (!text || text.startsWith("..")) return null;
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeEvidenceRef(relPath, lineNumber, label) {
|
|
62
|
+
const base = sanitizeRef(relPath);
|
|
63
|
+
if (!base) return null;
|
|
64
|
+
const suffix = Number.isFinite(lineNumber) && lineNumber > 0 ? `:${lineNumber}` : "";
|
|
65
|
+
const note = label ? ` (${sanitizeText(label, 80)})` : "";
|
|
66
|
+
return `${base}${suffix}${note}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function severityRank(severity) {
|
|
70
|
+
if (severity === "block") return 3;
|
|
71
|
+
if (severity === "warn") return 2;
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function surfaceRiskRank(risk) {
|
|
76
|
+
return {
|
|
77
|
+
none: 0,
|
|
78
|
+
low: 1,
|
|
79
|
+
medium: 2,
|
|
80
|
+
high: 3,
|
|
81
|
+
blocked: 4,
|
|
82
|
+
unknown: 1,
|
|
83
|
+
}[risk] ?? 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pickHigherRisk(current, next) {
|
|
87
|
+
return surfaceRiskRank(next) > surfaceRiskRank(current) ? next : current;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkWorkspace(cwd) {
|
|
91
|
+
const files = [];
|
|
92
|
+
|
|
93
|
+
function visit(current, depth) {
|
|
94
|
+
if (depth > 8 || files.length >= 2200) return;
|
|
95
|
+
let entries = [];
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (files.length >= 2200) return;
|
|
104
|
+
const absPath = path.join(current, entry.name);
|
|
105
|
+
const relPath = toPosix(path.relative(cwd, absPath));
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
108
|
+
visit(absPath, depth + 1);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!entry.isFile()) continue;
|
|
112
|
+
files.push({
|
|
113
|
+
absPath,
|
|
114
|
+
relPath,
|
|
115
|
+
text: undefined,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
visit(cwd, 0);
|
|
121
|
+
return files;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readText(file) {
|
|
125
|
+
if (file.text !== undefined) return file.text;
|
|
126
|
+
if (!(TEXT_FILE_RE.test(file.relPath) || ENV_FILE_RE.test(file.relPath))) {
|
|
127
|
+
file.text = null;
|
|
128
|
+
return file.text;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const stat = fs.statSync(file.absPath);
|
|
132
|
+
if (stat.size > 256 * 1024) {
|
|
133
|
+
file.text = null;
|
|
134
|
+
return file.text;
|
|
135
|
+
}
|
|
136
|
+
file.text = fs.readFileSync(file.absPath, "utf8");
|
|
137
|
+
return file.text;
|
|
138
|
+
} catch {
|
|
139
|
+
file.text = null;
|
|
140
|
+
return file.text;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findLineNumber(text, index) {
|
|
145
|
+
const prefix = text.slice(0, index);
|
|
146
|
+
return prefix.split(/\r?\n/).length;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectPatternRefs(file, regex, options = {}) {
|
|
150
|
+
const text = readText(file);
|
|
151
|
+
if (!text) return [];
|
|
152
|
+
const limit = options.limit || 5;
|
|
153
|
+
const refs = [];
|
|
154
|
+
const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
|
|
155
|
+
const matcher = new RegExp(regex.source, flags);
|
|
156
|
+
let match;
|
|
157
|
+
while ((match = matcher.exec(text)) && refs.length < limit) {
|
|
158
|
+
const lineNumber = findLineNumber(text, match.index);
|
|
159
|
+
refs.push(makeEvidenceRef(file.relPath, lineNumber, options.label ? options.label(match) : null));
|
|
160
|
+
if (match.index === matcher.lastIndex) matcher.lastIndex += 1;
|
|
161
|
+
}
|
|
162
|
+
return refs.filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectLineRefs(file, matcher, options = {}) {
|
|
166
|
+
const text = readText(file);
|
|
167
|
+
if (!text) return [];
|
|
168
|
+
const limit = options.limit || 5;
|
|
169
|
+
const refs = [];
|
|
170
|
+
const lines = text.split(/\r?\n/);
|
|
171
|
+
for (let index = 0; index < lines.length && refs.length < limit; index += 1) {
|
|
172
|
+
const line = lines[index];
|
|
173
|
+
const value = typeof matcher === "function" ? matcher(line, index + 1) : (matcher.test(line) ? true : false);
|
|
174
|
+
if (!value) continue;
|
|
175
|
+
const label = typeof value === "string"
|
|
176
|
+
? value
|
|
177
|
+
: options.label
|
|
178
|
+
? options.label(line, index + 1)
|
|
179
|
+
: null;
|
|
180
|
+
refs.push(makeEvidenceRef(file.relPath, index + 1, label));
|
|
181
|
+
}
|
|
182
|
+
return refs.filter(Boolean);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isClientFile(file) {
|
|
186
|
+
const relPath = file.relPath;
|
|
187
|
+
const text = readText(file) || "";
|
|
188
|
+
if (API_ROUTE_RE.test(relPath)) return false;
|
|
189
|
+
if (/^\s*["']use client["'];?/m.test(text)) return true;
|
|
190
|
+
return CLIENT_PATH_RE.test(relPath);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isServerFile(file) {
|
|
194
|
+
if (API_ROUTE_RE.test(file.relPath)) return true;
|
|
195
|
+
if (SERVER_PATH_RE.test(file.relPath)) return true;
|
|
196
|
+
const text = readText(file) || "";
|
|
197
|
+
return /\bimport\s+["']server-only["']/.test(text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isApiRouteFile(file) {
|
|
201
|
+
return API_ROUTE_RE.test(file.relPath);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isAdminRoute(file) {
|
|
205
|
+
return /(^|\/)(admin|admins)(\/|[-_.])/i.test(file.relPath);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function routeSensitivity(text) {
|
|
209
|
+
let score = 0;
|
|
210
|
+
if (/\b(delete|remove|destroy|drop|truncate)\b/i.test(text)) score += 2;
|
|
211
|
+
if (/\b(update|insert|upsert|create)\b/i.test(text)) score += 1;
|
|
212
|
+
if (SENSITIVE_NAME_RE.test(text)) score += 1;
|
|
213
|
+
return score;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function hasDbAccess(text) {
|
|
217
|
+
return /\b(prisma|drizzle|db|database|sql|supabase)\b/.test(text)
|
|
218
|
+
&& (
|
|
219
|
+
/\b(prisma\.\w+|db\.\w+|sql`|supabase\.(from|rpc|schema|storage)|\.(select|insert|update|delete|upsert)\s*\(|execute\s*\(|query\s*\()/i.test(text)
|
|
220
|
+
|| /\bfrom\s*\(\s*["'`][a-z0-9_]+["'`]\s*\)/i.test(text)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasAuthEvidence(text) {
|
|
225
|
+
return /\b(getServerSession|requireAuth|requireUser|auth\(\)|currentUser\(|clerkMiddleware|authMiddleware|supabase\.auth\.getUser|verifyAuth|getSession|session\.user|request\.user|ctx\.user)\b/i.test(text);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hasTenantScope(text) {
|
|
229
|
+
return /\b(userId|tenantId|organizationId|accountId|orgId|user_id|tenant_id|organization_id|account_id)\b/i.test(text)
|
|
230
|
+
|| /\bwhere\s*:\s*\{[\s\S]{0,120}\b(userId|tenantId|organizationId|accountId|orgId)\b/i.test(text)
|
|
231
|
+
|| /\beq\s*\(\s*["'`](user_id|tenant_id|organization_id|account_id|userId|tenantId|organizationId|accountId)["'`]/i.test(text);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function dbClientImportRef(file, text) {
|
|
235
|
+
const match = text.match(/\b(import|require)\b[\s\S]{0,160}(@prisma\/client|drizzle-orm|postgres|pg|supabase|\/db\b|\/server\b|server\/db|lib\/db|prisma|drizzle|service-role|service_role)/i);
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
const refs = collectPatternRefs(file, /(@prisma\/client|drizzle-orm|postgres|pg|supabase|\/db\b|\/server\b|server\/db|lib\/db|prisma|drizzle|service-role|service_role)/i, { limit: 1 });
|
|
238
|
+
return refs[0] || makeEvidenceRef(file.relPath, 1, null);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isSensitiveName(name) {
|
|
242
|
+
return SENSITIVE_NAME_RE.test(name);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseSqlTableName(fragment) {
|
|
246
|
+
const match = String(fragment || "").match(/\b(?:table|policy)\s+["']?([a-zA-Z0-9_.-]+)["']?/i);
|
|
247
|
+
return match ? match[1].replace(/^public\./i, "") : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractPolicyTable(text, startIndex) {
|
|
251
|
+
const slice = text.slice(startIndex, startIndex + 260);
|
|
252
|
+
const onMatch = slice.match(/\bon\s+["']?([a-zA-Z0-9_.-]+)["']?/i);
|
|
253
|
+
if (onMatch) return onMatch[1].replace(/^public\./i, "");
|
|
254
|
+
return parseSqlTableName(slice);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function collectSqlInsights(file) {
|
|
258
|
+
const text = readText(file);
|
|
259
|
+
if (!text) {
|
|
260
|
+
return {
|
|
261
|
+
rlsEnableRefs: [],
|
|
262
|
+
rlsDisableRefs: [],
|
|
263
|
+
policyRefs: [],
|
|
264
|
+
broadPolicyRefs: [],
|
|
265
|
+
broadWritePolicyRefs: [],
|
|
266
|
+
grantPublicRefs: [],
|
|
267
|
+
grantAnonRefs: [],
|
|
268
|
+
grantAllRefs: [],
|
|
269
|
+
tablesSeen: [],
|
|
270
|
+
sensitiveTables: [],
|
|
271
|
+
storagePublicRefs: [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tablesSeen = new Set();
|
|
276
|
+
const sensitiveTables = new Set();
|
|
277
|
+
const tablePattern = /\b(create|alter)\s+table\s+(?:if\s+not\s+exists\s+)?["']?([a-zA-Z0-9_.-]+)["']?/ig;
|
|
278
|
+
let tableMatch;
|
|
279
|
+
while ((tableMatch = tablePattern.exec(text))) {
|
|
280
|
+
const tableName = (tableMatch[2] || "").replace(/^public\./i, "");
|
|
281
|
+
if (!tableName) continue;
|
|
282
|
+
tablesSeen.add(tableName);
|
|
283
|
+
if (isSensitiveName(tableName)) sensitiveTables.add(tableName);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const policyRefs = collectPatternRefs(file, /\b(create|alter)\s+policy\b/ig, { limit: 8 });
|
|
287
|
+
const rlsEnableRefs = collectPatternRefs(file, /\balter\s+table\b[\s\S]{0,160}\benable\s+row\s+level\s+security\b/ig, { limit: 8 });
|
|
288
|
+
const rlsDisableRefs = collectPatternRefs(file, /\balter\s+table\b[\s\S]{0,160}\bdisable\s+row\s+level\s+security\b/ig, { limit: 8 });
|
|
289
|
+
|
|
290
|
+
const broadPolicyRefs = [];
|
|
291
|
+
const broadWritePolicyRefs = [];
|
|
292
|
+
const policyPattern = /\b(create|alter)\s+policy\b[\s\S]{0,500}?(using\s*\(\s*true\s*\)|with\s+check\s*\(\s*true\s*\))/ig;
|
|
293
|
+
let policyMatch;
|
|
294
|
+
while ((policyMatch = policyPattern.exec(text))) {
|
|
295
|
+
const lineNumber = findLineNumber(text, policyMatch.index);
|
|
296
|
+
const tableName = extractPolicyTable(text, policyMatch.index);
|
|
297
|
+
const label = tableName || null;
|
|
298
|
+
const ref = makeEvidenceRef(file.relPath, lineNumber, label);
|
|
299
|
+
if (!ref) continue;
|
|
300
|
+
broadPolicyRefs.push(ref);
|
|
301
|
+
const nearby = text.slice(policyMatch.index, policyMatch.index + 320);
|
|
302
|
+
if (/\bfor\s+(all|insert|update|delete)\b/i.test(nearby) || /with\s+check\s*\(\s*true\s*\)/i.test(nearby)) {
|
|
303
|
+
broadWritePolicyRefs.push(ref);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const grantPublicRefs = collectPatternRefs(file, /\bgrant\b[\s\S]{0,180}\bto\s+public\b/ig, { limit: 8 });
|
|
308
|
+
const grantAnonRefs = collectPatternRefs(file, /\bgrant\b[\s\S]{0,180}\bto\s+anon\b/ig, { limit: 8 });
|
|
309
|
+
const grantAllRefs = collectPatternRefs(file, /\bgrant\s+all\b/ig, { limit: 8 });
|
|
310
|
+
const storagePublicRefs = collectPatternRefs(file, /\b(storage\.buckets|storage\.objects|bucket)\b[\s\S]{0,220}\b(public|anon)\b/ig, { limit: 8 });
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
rlsEnableRefs,
|
|
314
|
+
rlsDisableRefs,
|
|
315
|
+
policyRefs,
|
|
316
|
+
broadPolicyRefs,
|
|
317
|
+
broadWritePolicyRefs,
|
|
318
|
+
grantPublicRefs,
|
|
319
|
+
grantAnonRefs,
|
|
320
|
+
grantAllRefs,
|
|
321
|
+
tablesSeen: [...tablesSeen],
|
|
322
|
+
sensitiveTables: [...sensitiveTables],
|
|
323
|
+
storagePublicRefs,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseEnvVariableNames(file) {
|
|
328
|
+
const text = readText(file);
|
|
329
|
+
if (!text) return [];
|
|
330
|
+
return text.split(/\r?\n/).flatMap((line, index) => {
|
|
331
|
+
const match = line.match(/^\s*([A-Z0-9_]+)\s*=/);
|
|
332
|
+
if (!match) return [];
|
|
333
|
+
return [{
|
|
334
|
+
name: match[1],
|
|
335
|
+
ref: makeEvidenceRef(file.relPath, index + 1, match[1]),
|
|
336
|
+
}];
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function makeBaseSurface() {
|
|
341
|
+
return {
|
|
342
|
+
risk: "none",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildEmptyReport() {
|
|
347
|
+
return {
|
|
348
|
+
schemaVersion: CONTRACT,
|
|
349
|
+
generatedAt: nowIso(),
|
|
350
|
+
status: "no_data",
|
|
351
|
+
redacted: true,
|
|
352
|
+
summary: {
|
|
353
|
+
surfacesInspected: 0,
|
|
354
|
+
risksFound: 0,
|
|
355
|
+
blockers: 0,
|
|
356
|
+
needsReview: 0,
|
|
357
|
+
safeNextAction: "No local data surface was detected. Re-run after adding database, storage, or API code.",
|
|
358
|
+
},
|
|
359
|
+
surfaces: {
|
|
360
|
+
supabase: {
|
|
361
|
+
detected: false,
|
|
362
|
+
migrations: [],
|
|
363
|
+
rlsEvidence: [],
|
|
364
|
+
policyEvidence: [],
|
|
365
|
+
storageEvidence: [],
|
|
366
|
+
risk: "none",
|
|
367
|
+
},
|
|
368
|
+
postgres: {
|
|
369
|
+
detected: false,
|
|
370
|
+
migrations: [],
|
|
371
|
+
grants: [],
|
|
372
|
+
risk: "none",
|
|
373
|
+
},
|
|
374
|
+
prisma: {
|
|
375
|
+
detected: false,
|
|
376
|
+
schemaRefs: [],
|
|
377
|
+
clientExposureRefs: [],
|
|
378
|
+
risk: "none",
|
|
379
|
+
},
|
|
380
|
+
drizzle: {
|
|
381
|
+
detected: false,
|
|
382
|
+
schemaRefs: [],
|
|
383
|
+
migrationRefs: [],
|
|
384
|
+
risk: "none",
|
|
385
|
+
},
|
|
386
|
+
apiRoutes: {
|
|
387
|
+
detected: false,
|
|
388
|
+
routeRefs: [],
|
|
389
|
+
authEvidence: [],
|
|
390
|
+
missingAuthSignals: [],
|
|
391
|
+
risk: "none",
|
|
392
|
+
},
|
|
393
|
+
storage: {
|
|
394
|
+
detected: false,
|
|
395
|
+
bucketRefs: [],
|
|
396
|
+
publicAccessSignals: [],
|
|
397
|
+
risk: "none",
|
|
398
|
+
},
|
|
399
|
+
envAndSecrets: {
|
|
400
|
+
detected: false,
|
|
401
|
+
secretNameRefs: [],
|
|
402
|
+
clientExposureSignals: [],
|
|
403
|
+
risk: "none",
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
findings: [],
|
|
407
|
+
rollup: {
|
|
408
|
+
readySignals: [],
|
|
409
|
+
needsReview: [],
|
|
410
|
+
blockedItems: [],
|
|
411
|
+
notEnoughEvidence: [],
|
|
412
|
+
},
|
|
413
|
+
integrationRefs: {
|
|
414
|
+
productionConfidence: [REPORT_JSON_REL],
|
|
415
|
+
productHome: [REPORT_JSON_REL],
|
|
416
|
+
radar: [REPORT_JSON_REL],
|
|
417
|
+
status: [REPORT_JSON_REL],
|
|
418
|
+
dashboard: [REPORT_JSON_REL],
|
|
419
|
+
support: [REPORT_JSON_REL],
|
|
420
|
+
},
|
|
421
|
+
privacy: {
|
|
422
|
+
localOnly: true,
|
|
423
|
+
remoteFetch: false,
|
|
424
|
+
dbConnection: false,
|
|
425
|
+
rawSecretValuesStored: false,
|
|
426
|
+
rawPromptStored: false,
|
|
427
|
+
rawCodeStored: false,
|
|
428
|
+
fullLogsStored: false,
|
|
429
|
+
privatePathsStored: false,
|
|
430
|
+
redacted: true,
|
|
431
|
+
},
|
|
432
|
+
claimSafety: {
|
|
433
|
+
productionCertified: false,
|
|
434
|
+
securityGuaranteed: false,
|
|
435
|
+
liveDatabaseScanned: false,
|
|
436
|
+
rlsAutoFixed: false,
|
|
437
|
+
cloudScanner: false,
|
|
438
|
+
launchReadyClaim: false,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function addFinding(report, finding) {
|
|
444
|
+
const normalized = {
|
|
445
|
+
id: finding.id,
|
|
446
|
+
severity: finding.severity,
|
|
447
|
+
category: finding.category,
|
|
448
|
+
title: sanitizeText(finding.title, 120),
|
|
449
|
+
evidenceRef: sanitizeRef(finding.evidenceRef),
|
|
450
|
+
reasonCodes: unique(finding.reasonCodes || []),
|
|
451
|
+
safeNextAction: sanitizeText(finding.safeNextAction, 220) || "Review the local data boundary before shipping.",
|
|
452
|
+
};
|
|
453
|
+
report.findings.push(normalized);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function pushInfoFinding(report, fields) {
|
|
457
|
+
addFinding(report, {
|
|
458
|
+
severity: "info",
|
|
459
|
+
...fields,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function pushWarnFinding(report, fields) {
|
|
464
|
+
addFinding(report, {
|
|
465
|
+
severity: "warn",
|
|
466
|
+
...fields,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function pushBlockFinding(report, fields) {
|
|
471
|
+
addFinding(report, {
|
|
472
|
+
severity: "block",
|
|
473
|
+
...fields,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function buildDataPermissionsGuard(cwd = process.cwd(), options = {}) {
|
|
478
|
+
const report = buildEmptyReport();
|
|
479
|
+
const files = walkWorkspace(cwd);
|
|
480
|
+
|
|
481
|
+
const sqlFiles = files.filter((file) => SQL_FILE_RE.test(file.relPath));
|
|
482
|
+
const envFiles = files.filter((file) => ENV_FILE_RE.test(file.relPath));
|
|
483
|
+
const prismaFiles = files.filter((file) => PRISMA_FILE_RE.test(file.relPath));
|
|
484
|
+
const drizzleFiles = files.filter((file) => DRIZZLE_CONFIG_RE.test(file.relPath) || /(^|\/)(drizzle|db|database)\/.*schema\.(js|ts)$/i.test(file.relPath));
|
|
485
|
+
const apiRouteFiles = files.filter((file) => isApiRouteFile(file));
|
|
486
|
+
const supabaseFiles = files.filter((file) => /(^|\/)supabase\//i.test(file.relPath));
|
|
487
|
+
const sourceFiles = files.filter((file) => /\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/i.test(file.relPath));
|
|
488
|
+
const workspaceMiddlewareAuthRefs = unique(sourceFiles.flatMap((file) => {
|
|
489
|
+
if (!/(^|\/)middleware\.(cjs|cts|js|mjs|mts|ts)$/i.test(file.relPath)) return [];
|
|
490
|
+
const text = readText(file) || "";
|
|
491
|
+
if (!hasAuthEvidence(text)) return [];
|
|
492
|
+
return collectPatternRefs(file, /\b(getServerSession|requireAuth|requireUser|auth\(\)|currentUser\(|clerkMiddleware|authMiddleware|supabase\.auth\.getUser|verifyAuth|getSession|session\.user|request\.user|ctx\.user)\b/i, { limit: 1 });
|
|
493
|
+
})).slice(0, 4);
|
|
494
|
+
|
|
495
|
+
const supabaseSurface = report.surfaces.supabase;
|
|
496
|
+
const postgresSurface = report.surfaces.postgres;
|
|
497
|
+
const prismaSurface = report.surfaces.prisma;
|
|
498
|
+
const drizzleSurface = report.surfaces.drizzle;
|
|
499
|
+
const apiSurface = report.surfaces.apiRoutes;
|
|
500
|
+
const storageSurface = report.surfaces.storage;
|
|
501
|
+
const envSurface = report.surfaces.envAndSecrets;
|
|
502
|
+
|
|
503
|
+
if (supabaseFiles.length > 0) {
|
|
504
|
+
supabaseSurface.detected = true;
|
|
505
|
+
supabaseSurface.migrations = unique(
|
|
506
|
+
supabaseFiles
|
|
507
|
+
.filter((file) => /(^|\/)supabase\/migrations\/.*\.sql$/i.test(file.relPath))
|
|
508
|
+
.map((file) => sanitizeRef(file.relPath))
|
|
509
|
+
).slice(0, 8);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const sqlInsights = sqlFiles.map((file) => ({
|
|
513
|
+
file,
|
|
514
|
+
insights: collectSqlInsights(file),
|
|
515
|
+
}));
|
|
516
|
+
|
|
517
|
+
for (const entry of sqlInsights) {
|
|
518
|
+
const file = entry.file;
|
|
519
|
+
const insights = entry.insights;
|
|
520
|
+
const isSupabase = /(^|\/)supabase\/migrations\/.*\.sql$/i.test(file.relPath);
|
|
521
|
+
const isMigration = /(^|\/)(db\/migrations|migrations|sql|supabase\/migrations)\//i.test(file.relPath) || /schema\.sql$/i.test(file.relPath);
|
|
522
|
+
|
|
523
|
+
if (insights.tablesSeen.length > 0 || insights.policyRefs.length > 0 || insights.grantPublicRefs.length > 0 || insights.grantAnonRefs.length > 0) {
|
|
524
|
+
postgresSurface.detected = true;
|
|
525
|
+
postgresSurface.migrations.push(sanitizeRef(file.relPath));
|
|
526
|
+
}
|
|
527
|
+
if (isSupabase) {
|
|
528
|
+
supabaseSurface.detected = true;
|
|
529
|
+
supabaseSurface.rlsEvidence.push(...insights.rlsEnableRefs);
|
|
530
|
+
supabaseSurface.policyEvidence.push(...insights.policyRefs);
|
|
531
|
+
supabaseSurface.storageEvidence.push(...insights.storagePublicRefs);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
postgresSurface.grants.push(...insights.grantPublicRefs, ...insights.grantAnonRefs, ...insights.grantAllRefs);
|
|
535
|
+
|
|
536
|
+
if (insights.rlsEnableRefs.length > 0) {
|
|
537
|
+
pushInfoFinding(report, {
|
|
538
|
+
id: `rls-enabled-${file.relPath}`,
|
|
539
|
+
category: "rls",
|
|
540
|
+
title: "Row-level security evidence found",
|
|
541
|
+
evidenceRef: insights.rlsEnableRefs[0],
|
|
542
|
+
reasonCodes: ["rls_enabled_detected", "local_static_scan_only", "safe_next_action_available"],
|
|
543
|
+
safeNextAction: "Keep the visible RLS and policy evidence current as schema changes land.",
|
|
544
|
+
});
|
|
545
|
+
if (isSupabase) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "low");
|
|
546
|
+
if (isMigration) postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "low");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (insights.policyRefs.length > 0) {
|
|
550
|
+
pushInfoFinding(report, {
|
|
551
|
+
id: `policy-${file.relPath}`,
|
|
552
|
+
category: "policy",
|
|
553
|
+
title: "Policy evidence found",
|
|
554
|
+
evidenceRef: insights.policyRefs[0],
|
|
555
|
+
reasonCodes: ["policy_detected", "local_static_scan_only", "safe_next_action_available"],
|
|
556
|
+
safeNextAction: "Review policy changes alongside any new table or route changes.",
|
|
557
|
+
});
|
|
558
|
+
if (isSupabase) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "low");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const ref of insights.rlsDisableRefs.slice(0, 3)) {
|
|
562
|
+
pushBlockFinding(report, {
|
|
563
|
+
id: `rls-disabled-${ref}`,
|
|
564
|
+
category: "rls",
|
|
565
|
+
title: "Row-level security is disabled locally",
|
|
566
|
+
evidenceRef: ref,
|
|
567
|
+
reasonCodes: ["rls_disabled_detected", "blocked_data_exposure", "production_data_risk", "safe_next_action_available"],
|
|
568
|
+
safeNextAction: "Re-enable RLS or document a narrower server-only data path before shipping.",
|
|
569
|
+
});
|
|
570
|
+
if (isSupabase) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "blocked");
|
|
571
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "blocked");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const ref of insights.broadPolicyRefs.slice(0, 3)) {
|
|
575
|
+
pushWarnFinding(report, {
|
|
576
|
+
id: `policy-broad-${ref}`,
|
|
577
|
+
category: "policy",
|
|
578
|
+
title: "Broad policy condition needs review",
|
|
579
|
+
evidenceRef: ref,
|
|
580
|
+
reasonCodes: ["broad_policy_detected", "policy_true_condition_detected", "not_enough_evidence", "safe_next_action_available"],
|
|
581
|
+
safeNextAction: "Review broad policy conditions and tighten the user or tenant boundary before shipping.",
|
|
582
|
+
});
|
|
583
|
+
if (isSupabase) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "medium");
|
|
584
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "medium");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const ref of insights.broadWritePolicyRefs.slice(0, 3)) {
|
|
588
|
+
pushBlockFinding(report, {
|
|
589
|
+
id: `policy-broad-write-${ref}`,
|
|
590
|
+
category: "policy",
|
|
591
|
+
title: "Broad write or delete policy is exposed locally",
|
|
592
|
+
evidenceRef: ref,
|
|
593
|
+
reasonCodes: ["broad_policy_detected", "policy_true_condition_detected", "blocked_data_exposure", "safe_next_action_available"],
|
|
594
|
+
safeNextAction: "Tighten the write or delete policy so it is scoped to the right user or tenant before shipping.",
|
|
595
|
+
});
|
|
596
|
+
if (isSupabase) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "blocked");
|
|
597
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "blocked");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (const ref of insights.grantPublicRefs.slice(0, 3)) {
|
|
601
|
+
pushBlockFinding(report, {
|
|
602
|
+
id: `grant-public-${ref}`,
|
|
603
|
+
category: "grant",
|
|
604
|
+
title: "Public database grant is visible locally",
|
|
605
|
+
evidenceRef: ref,
|
|
606
|
+
reasonCodes: ["grant_public_detected", "blocked_data_exposure", "production_data_risk", "safe_next_action_available"],
|
|
607
|
+
safeNextAction: "Remove or narrow the public grant before treating the data boundary as safe.",
|
|
608
|
+
});
|
|
609
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "blocked");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
for (const ref of insights.grantAllRefs.slice(0, 3)) {
|
|
613
|
+
pushBlockFinding(report, {
|
|
614
|
+
id: `grant-all-${ref}`,
|
|
615
|
+
category: "grant",
|
|
616
|
+
title: "Broad GRANT ALL is visible locally",
|
|
617
|
+
evidenceRef: ref,
|
|
618
|
+
reasonCodes: ["grant_all_detected", "blocked_data_exposure", "production_data_risk", "safe_next_action_available"],
|
|
619
|
+
safeNextAction: "Replace broad grants with the narrowest roles needed before shipping.",
|
|
620
|
+
});
|
|
621
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "blocked");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for (const ref of insights.grantAnonRefs.slice(0, 3)) {
|
|
625
|
+
const severity = insights.sensitiveTables.length > 0 ? "block" : "warn";
|
|
626
|
+
addFinding(report, {
|
|
627
|
+
id: `grant-anon-${ref}`,
|
|
628
|
+
severity,
|
|
629
|
+
category: "grant",
|
|
630
|
+
title: insights.sensitiveTables.length > 0
|
|
631
|
+
? "Anon access to a sensitive table is visible locally"
|
|
632
|
+
: "Anon grant needs review",
|
|
633
|
+
evidenceRef: ref,
|
|
634
|
+
reasonCodes: [
|
|
635
|
+
"grant_anon_detected",
|
|
636
|
+
...(insights.sensitiveTables.length > 0 ? ["sensitive_table_detected", "production_data_risk"] : ["not_enough_evidence"]),
|
|
637
|
+
"safe_next_action_available",
|
|
638
|
+
],
|
|
639
|
+
safeNextAction: insights.sensitiveTables.length > 0
|
|
640
|
+
? "Narrow or remove anon access before shipping sensitive data paths."
|
|
641
|
+
: "Review whether anon access is intentional and bounded before shipping.",
|
|
642
|
+
});
|
|
643
|
+
postgresSurface.risk = pickHigherRisk(postgresSurface.risk, insights.sensitiveTables.length > 0 ? "blocked" : "medium");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
for (const ref of insights.storagePublicRefs.slice(0, 3)) {
|
|
647
|
+
storageSurface.detected = true;
|
|
648
|
+
storageSurface.bucketRefs.push(ref);
|
|
649
|
+
storageSurface.publicAccessSignals.push(ref);
|
|
650
|
+
if (isSupabase) {
|
|
651
|
+
supabaseSurface.storageEvidence.push(ref);
|
|
652
|
+
}
|
|
653
|
+
const severity = SENSITIVE_BUCKET_RE.test(readText(file) || "") ? "block" : "warn";
|
|
654
|
+
addFinding(report, {
|
|
655
|
+
id: `storage-sql-${ref}`,
|
|
656
|
+
severity,
|
|
657
|
+
category: "storage",
|
|
658
|
+
title: severity === "block"
|
|
659
|
+
? "Sensitive storage bucket appears public"
|
|
660
|
+
: "Public storage bucket needs review",
|
|
661
|
+
evidenceRef: ref,
|
|
662
|
+
reasonCodes: [
|
|
663
|
+
"storage_detected",
|
|
664
|
+
"storage_public_bucket_detected",
|
|
665
|
+
...(severity === "block" ? ["sensitive_storage_public_risk", "production_data_risk"] : ["not_enough_evidence"]),
|
|
666
|
+
"safe_next_action_available",
|
|
667
|
+
],
|
|
668
|
+
safeNextAction: severity === "block"
|
|
669
|
+
? "Restrict the sensitive bucket or move the files behind a narrower policy before shipping."
|
|
670
|
+
: "Review whether the bucket should stay public and keep sensitive files out of that path.",
|
|
671
|
+
});
|
|
672
|
+
storageSurface.risk = pickHigherRisk(storageSurface.risk, severity === "block" ? "blocked" : "medium");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
supabaseSurface.rlsEvidence = unique(supabaseSurface.rlsEvidence).slice(0, 8);
|
|
677
|
+
supabaseSurface.policyEvidence = unique(supabaseSurface.policyEvidence).slice(0, 8);
|
|
678
|
+
supabaseSurface.storageEvidence = unique(supabaseSurface.storageEvidence).slice(0, 8);
|
|
679
|
+
postgresSurface.migrations = unique(postgresSurface.migrations).slice(0, 8);
|
|
680
|
+
postgresSurface.grants = unique(postgresSurface.grants).slice(0, 8);
|
|
681
|
+
|
|
682
|
+
const supabaseConfig = files.find((file) => /(^|\/)supabase\/config\.toml$/i.test(file.relPath));
|
|
683
|
+
if (supabaseConfig) {
|
|
684
|
+
supabaseSurface.detected = true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if ((supabaseSurface.detected || postgresSurface.detected) && supabaseSurface.rlsEvidence.length === 0 && supabaseSurface.policyEvidence.length === 0) {
|
|
688
|
+
pushWarnFinding(report, {
|
|
689
|
+
id: "rls-missing-or-unclear",
|
|
690
|
+
category: "rls",
|
|
691
|
+
title: "Data boundary needs evidence",
|
|
692
|
+
evidenceRef: supabaseSurface.migrations[0] || postgresSurface.migrations[0] || sanitizeRef(supabaseConfig?.relPath),
|
|
693
|
+
reasonCodes: ["rls_missing_or_unclear", "not_enough_evidence", "local_static_scan_only", "safe_next_action_available"],
|
|
694
|
+
safeNextAction: "Add visible RLS or policy evidence before treating the local data boundary as safe.",
|
|
695
|
+
});
|
|
696
|
+
if (supabaseSurface.detected) supabaseSurface.risk = pickHigherRisk(supabaseSurface.risk, "medium");
|
|
697
|
+
if (postgresSurface.detected) postgresSurface.risk = pickHigherRisk(postgresSurface.risk, "medium");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (prismaFiles.length > 0) {
|
|
701
|
+
prismaSurface.detected = true;
|
|
702
|
+
prismaSurface.schemaRefs = prismaFiles.map((file) => sanitizeRef(file.relPath)).filter(Boolean).slice(0, 8);
|
|
703
|
+
for (const file of prismaFiles) {
|
|
704
|
+
const text = readText(file) || "";
|
|
705
|
+
const modelPattern = /\bmodel\s+([A-Za-z0-9_]+)\s*\{([\s\S]*?)\n\}/g;
|
|
706
|
+
let modelMatch;
|
|
707
|
+
let foundScopedModel = false;
|
|
708
|
+
let foundSensitiveModel = false;
|
|
709
|
+
while ((modelMatch = modelPattern.exec(text))) {
|
|
710
|
+
const modelName = modelMatch[1];
|
|
711
|
+
const body = modelMatch[2] || "";
|
|
712
|
+
if (!isSensitiveName(modelName)) continue;
|
|
713
|
+
foundSensitiveModel = true;
|
|
714
|
+
const lineNumber = findLineNumber(text, modelMatch.index);
|
|
715
|
+
const ref = makeEvidenceRef(file.relPath, lineNumber, modelName);
|
|
716
|
+
if (/\b(userId|tenantId|organizationId|accountId|orgId)\b/.test(body)) {
|
|
717
|
+
foundScopedModel = true;
|
|
718
|
+
pushInfoFinding(report, {
|
|
719
|
+
id: `prisma-scope-${modelName}-${lineNumber}`,
|
|
720
|
+
category: "tenant_scope",
|
|
721
|
+
title: "Schema scope field found",
|
|
722
|
+
evidenceRef: ref,
|
|
723
|
+
reasonCodes: ["prisma_detected", "tenant_scope_evidence_detected", "safe_next_action_available"],
|
|
724
|
+
safeNextAction: "Keep user or tenant scope fields aligned with the corresponding route and policy checks.",
|
|
725
|
+
});
|
|
726
|
+
prismaSurface.risk = pickHigherRisk(prismaSurface.risk, "low");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (foundSensitiveModel && !foundScopedModel) {
|
|
730
|
+
const ref = collectPatternRefs(file, /\bmodel\s+([A-Za-z0-9_]+)\s*\{/i, { limit: 1, label: (match) => match[1] || null })[0];
|
|
731
|
+
pushWarnFinding(report, {
|
|
732
|
+
id: `prisma-sensitive-${file.relPath}`,
|
|
733
|
+
category: "tenant_scope",
|
|
734
|
+
title: "Sensitive Prisma model needs access evidence",
|
|
735
|
+
evidenceRef: ref,
|
|
736
|
+
reasonCodes: ["prisma_detected", "sensitive_model_detected", "tenant_scope_missing_or_unclear", "not_enough_evidence", "safe_next_action_available"],
|
|
737
|
+
safeNextAction: "Add visible route, policy, or tenant-scoping evidence before treating sensitive Prisma models as safe to ship.",
|
|
738
|
+
});
|
|
739
|
+
prismaSurface.risk = pickHigherRisk(prismaSurface.risk, "medium");
|
|
740
|
+
}
|
|
741
|
+
if (foundSensitiveModel && prismaSurface.risk === "none") {
|
|
742
|
+
prismaSurface.risk = pickHigherRisk(prismaSurface.risk, "medium");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (drizzleFiles.length > 0) {
|
|
748
|
+
drizzleSurface.detected = true;
|
|
749
|
+
drizzleSurface.schemaRefs = unique(
|
|
750
|
+
drizzleFiles
|
|
751
|
+
.filter((file) => /schema\.(js|ts)$/i.test(file.relPath))
|
|
752
|
+
.map((file) => sanitizeRef(file.relPath))
|
|
753
|
+
).slice(0, 8);
|
|
754
|
+
drizzleSurface.migrationRefs = unique(
|
|
755
|
+
files
|
|
756
|
+
.filter((file) => /(^|\/)drizzle\/.*\.(sql|js|ts)$/i.test(file.relPath))
|
|
757
|
+
.map((file) => sanitizeRef(file.relPath))
|
|
758
|
+
).slice(0, 8);
|
|
759
|
+
let drizzleHasScope = false;
|
|
760
|
+
for (const file of drizzleFiles) {
|
|
761
|
+
const text = readText(file) || "";
|
|
762
|
+
if (/\bpgTable\s*\(/.test(text) && /\b(userId|tenantId|organizationId|accountId|orgId)\b/.test(text)) {
|
|
763
|
+
drizzleHasScope = true;
|
|
764
|
+
const ref = collectPatternRefs(file, /\b(userId|tenantId|organizationId|accountId|orgId)\b/i, { limit: 1 })[0];
|
|
765
|
+
pushInfoFinding(report, {
|
|
766
|
+
id: `drizzle-scope-${file.relPath}`,
|
|
767
|
+
category: "tenant_scope",
|
|
768
|
+
title: "Schema scope field found",
|
|
769
|
+
evidenceRef: ref,
|
|
770
|
+
reasonCodes: ["drizzle_detected", "tenant_scope_evidence_detected", "safe_next_action_available"],
|
|
771
|
+
safeNextAction: "Keep Drizzle scope fields aligned with the route-level auth and tenant checks.",
|
|
772
|
+
});
|
|
773
|
+
drizzleSurface.risk = pickHigherRisk(drizzleSurface.risk, "low");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!drizzleHasScope && drizzleSurface.detected) {
|
|
777
|
+
pushWarnFinding(report, {
|
|
778
|
+
id: "drizzle-scope-missing",
|
|
779
|
+
category: "tenant_scope",
|
|
780
|
+
title: "Tenant or user scope is unclear",
|
|
781
|
+
evidenceRef: drizzleSurface.schemaRefs[0] || drizzleSurface.migrationRefs[0],
|
|
782
|
+
reasonCodes: ["drizzle_detected", "tenant_scope_missing_or_unclear", "not_enough_evidence", "safe_next_action_available"],
|
|
783
|
+
safeNextAction: "Add visible tenant or user scoping evidence before treating the Drizzle data path as safe.",
|
|
784
|
+
});
|
|
785
|
+
drizzleSurface.risk = pickHigherRisk(drizzleSurface.risk, "medium");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
for (const file of envFiles) {
|
|
790
|
+
const vars = parseEnvVariableNames(file);
|
|
791
|
+
if (vars.length > 0) envSurface.detected = true;
|
|
792
|
+
for (const item of vars) {
|
|
793
|
+
if (/(SERVICE_ROLE|DATABASE_URL|SECRET|TOKEN|ADMIN)/i.test(item.name)) {
|
|
794
|
+
envSurface.secretNameRefs.push(item.ref);
|
|
795
|
+
}
|
|
796
|
+
if (/^NEXT_PUBLIC_.*(SERVICE_ROLE|DATABASE_URL|SECRET|TOKEN|ADMIN)/i.test(item.name)) {
|
|
797
|
+
pushBlockFinding(report, {
|
|
798
|
+
id: `next-public-secret-${item.name}`,
|
|
799
|
+
category: "env",
|
|
800
|
+
title: "Public env name looks like a secret or admin path",
|
|
801
|
+
evidenceRef: item.ref,
|
|
802
|
+
reasonCodes: ["next_public_secret_like_name", "blocked_data_exposure", "production_data_risk", "safe_next_action_available"],
|
|
803
|
+
safeNextAction: "Rename the public env and move the secret or admin usage behind a server-only boundary before shipping.",
|
|
804
|
+
});
|
|
805
|
+
envSurface.clientExposureSignals.push(item.ref);
|
|
806
|
+
envSurface.risk = pickHigherRisk(envSurface.risk, "blocked");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
for (const file of sourceFiles) {
|
|
812
|
+
const text = readText(file);
|
|
813
|
+
if (!text) continue;
|
|
814
|
+
const clientFile = isClientFile(file);
|
|
815
|
+
const serverFile = isServerFile(file);
|
|
816
|
+
const authEvidence = hasAuthEvidence(text);
|
|
817
|
+
const tenantEvidence = hasTenantScope(text);
|
|
818
|
+
const dbAccess = hasDbAccess(text);
|
|
819
|
+
const adminRoute = isAdminRoute(file);
|
|
820
|
+
const routeRef = makeEvidenceRef(file.relPath, 1, null);
|
|
821
|
+
|
|
822
|
+
if (/supabase\/config\.toml/i.test(file.relPath) || /createClient/i.test(text) && /supabase/i.test(text)) {
|
|
823
|
+
supabaseSurface.detected = true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (isApiRouteFile(file)) {
|
|
827
|
+
apiSurface.detected = true;
|
|
828
|
+
apiSurface.routeRefs.push(routeRef);
|
|
829
|
+
if (authEvidence) {
|
|
830
|
+
const ref = collectPatternRefs(file, /\b(getServerSession|requireAuth|requireUser|auth\(\)|currentUser\(|clerkMiddleware|authMiddleware|supabase\.auth\.getUser|verifyAuth|getSession|session\.user|request\.user|ctx\.user)\b/i, { limit: 1 })[0];
|
|
831
|
+
if (ref) apiSurface.authEvidence.push(ref);
|
|
832
|
+
} else if (workspaceMiddlewareAuthRefs.length > 0) {
|
|
833
|
+
apiSurface.authEvidence.push(workspaceMiddlewareAuthRefs[0]);
|
|
834
|
+
}
|
|
835
|
+
if (dbAccess && !authEvidence) {
|
|
836
|
+
const ref = collectPatternRefs(file, /\b(prisma\.\w+|db\.\w+|sql`|supabase\.(from|rpc|schema|storage)|\.(select|insert|update|delete|upsert)\s*\(|execute\s*\(|query\s*\()/i, { limit: 1 })[0] || routeRef;
|
|
837
|
+
apiSurface.missingAuthSignals.push(ref);
|
|
838
|
+
pushWarnFinding(report, {
|
|
839
|
+
id: `api-auth-${file.relPath}`,
|
|
840
|
+
category: "api_auth",
|
|
841
|
+
title: workspaceMiddlewareAuthRefs.length > 0
|
|
842
|
+
? "API route uses data access without route-local auth evidence"
|
|
843
|
+
: "API route uses data access without clear auth evidence",
|
|
844
|
+
evidenceRef: ref,
|
|
845
|
+
reasonCodes: [
|
|
846
|
+
"api_route_detected",
|
|
847
|
+
"api_auth_missing_or_unclear",
|
|
848
|
+
...(workspaceMiddlewareAuthRefs.length > 0 ? ["api_auth_evidence_detected"] : []),
|
|
849
|
+
"not_enough_evidence",
|
|
850
|
+
"safe_next_action_available",
|
|
851
|
+
],
|
|
852
|
+
safeNextAction: workspaceMiddlewareAuthRefs.length > 0
|
|
853
|
+
? "Keep middleware auth in place and add route-local auth or session evidence next to the data access before shipping."
|
|
854
|
+
: "Add visible auth or session checks next to the route's data access before shipping.",
|
|
855
|
+
});
|
|
856
|
+
apiSurface.risk = pickHigherRisk(apiSurface.risk, "medium");
|
|
857
|
+
}
|
|
858
|
+
if (dbAccess && authEvidence && !tenantEvidence && /organization|tenant|account|customer|team/i.test(text)) {
|
|
859
|
+
const ref = collectPatternRefs(file, /\b(prisma\.\w+|db\.\w+|sql`|supabase\.(from|rpc|schema|storage)|\.(select|insert|update|delete|upsert)\s*\(|execute\s*\(|query\s*\()/i, { limit: 1 })[0] || routeRef;
|
|
860
|
+
apiSurface.missingAuthSignals.push(ref);
|
|
861
|
+
pushWarnFinding(report, {
|
|
862
|
+
id: `api-scope-${file.relPath}`,
|
|
863
|
+
category: "tenant_scope",
|
|
864
|
+
title: "Tenant or user scoping is unclear",
|
|
865
|
+
evidenceRef: ref,
|
|
866
|
+
reasonCodes: ["api_route_detected", "tenant_scope_missing_or_unclear", "not_enough_evidence", "safe_next_action_available"],
|
|
867
|
+
safeNextAction: "Add visible user, tenant, organization, or account filters near the data access before shipping.",
|
|
868
|
+
});
|
|
869
|
+
apiSurface.risk = pickHigherRisk(apiSurface.risk, "medium");
|
|
870
|
+
}
|
|
871
|
+
if (dbAccess && authEvidence) {
|
|
872
|
+
const ref = collectPatternRefs(file, /\b(getServerSession|requireAuth|requireUser|auth\(\)|currentUser\(|clerkMiddleware|authMiddleware|supabase\.auth\.getUser|verifyAuth|getSession|session\.user|request\.user|ctx\.user)\b/i, { limit: 1 })[0] || routeRef;
|
|
873
|
+
pushInfoFinding(report, {
|
|
874
|
+
id: `api-auth-positive-${file.relPath}`,
|
|
875
|
+
category: "api_auth",
|
|
876
|
+
title: tenantEvidence
|
|
877
|
+
? "Route auth and scope evidence found"
|
|
878
|
+
: "Route auth evidence found",
|
|
879
|
+
evidenceRef: ref,
|
|
880
|
+
reasonCodes: [
|
|
881
|
+
"api_route_detected",
|
|
882
|
+
"api_auth_evidence_detected",
|
|
883
|
+
...(tenantEvidence ? ["tenant_scope_evidence_detected"] : []),
|
|
884
|
+
"safe_next_action_available",
|
|
885
|
+
],
|
|
886
|
+
safeNextAction: tenantEvidence
|
|
887
|
+
? "Keep the route auth and scope checks aligned with future data changes."
|
|
888
|
+
: "Keep route auth evidence visible and add tenant or user scoping where needed.",
|
|
889
|
+
});
|
|
890
|
+
apiSurface.risk = pickHigherRisk(apiSurface.risk, tenantEvidence ? "low" : "medium");
|
|
891
|
+
}
|
|
892
|
+
if (adminRoute && dbAccess && !authEvidence) {
|
|
893
|
+
const severity = routeSensitivity(text) >= 3 ? "block" : "warn";
|
|
894
|
+
addFinding(report, {
|
|
895
|
+
id: `admin-route-${file.relPath}`,
|
|
896
|
+
severity,
|
|
897
|
+
category: "api_auth",
|
|
898
|
+
title: severity === "block"
|
|
899
|
+
? "Admin data route lacks visible auth guard"
|
|
900
|
+
: "Admin route needs visible auth evidence",
|
|
901
|
+
evidenceRef: routeRef,
|
|
902
|
+
reasonCodes: [
|
|
903
|
+
"api_route_detected",
|
|
904
|
+
"api_auth_missing_or_unclear",
|
|
905
|
+
"production_data_risk",
|
|
906
|
+
"safe_next_action_available",
|
|
907
|
+
],
|
|
908
|
+
safeNextAction: "Add an explicit admin auth guard and visible authorization boundary before shipping the admin route.",
|
|
909
|
+
});
|
|
910
|
+
apiSurface.risk = pickHigherRisk(apiSurface.risk, severity === "block" ? "blocked" : "high");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const serviceRoleRef = collectPatternRefs(file, /\b(SUPABASE_SERVICE_ROLE_KEY|service_role|serviceRole|createServiceRoleClient|supabaseAdmin|adminClient)\b/i, { limit: 1 })[0];
|
|
915
|
+
if (serviceRoleRef) {
|
|
916
|
+
envSurface.detected = true;
|
|
917
|
+
envSurface.secretNameRefs.push(serviceRoleRef);
|
|
918
|
+
if (clientFile) {
|
|
919
|
+
pushBlockFinding(report, {
|
|
920
|
+
id: `service-role-client-${file.relPath}`,
|
|
921
|
+
category: "service_role",
|
|
922
|
+
title: "Service-role or admin client is exposed to client code",
|
|
923
|
+
evidenceRef: serviceRoleRef,
|
|
924
|
+
reasonCodes: ["service_role_reference_detected", "service_role_client_exposure", "blocked_data_exposure", "safe_next_action_available"],
|
|
925
|
+
safeNextAction: "Move the service-role or admin client behind a server-only boundary before shipping.",
|
|
926
|
+
});
|
|
927
|
+
envSurface.clientExposureSignals.push(serviceRoleRef);
|
|
928
|
+
envSurface.risk = pickHigherRisk(envSurface.risk, "blocked");
|
|
929
|
+
} else if (serverFile) {
|
|
930
|
+
pushWarnFinding(report, {
|
|
931
|
+
id: `service-role-server-${file.relPath}`,
|
|
932
|
+
category: "service_role",
|
|
933
|
+
title: "Service-role reference needs manual review",
|
|
934
|
+
evidenceRef: serviceRoleRef,
|
|
935
|
+
reasonCodes: ["service_role_reference_detected", "env_reference_detected", "not_enough_evidence", "safe_next_action_available"],
|
|
936
|
+
safeNextAction: "Confirm the service-role or admin key stays server-only and is not reachable from the browser path.",
|
|
937
|
+
});
|
|
938
|
+
envSurface.risk = pickHigherRisk(envSurface.risk, "medium");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const databaseUrlRef = collectPatternRefs(file, /\bDATABASE_URL\b/i, { limit: 1 })[0];
|
|
943
|
+
if (databaseUrlRef) {
|
|
944
|
+
envSurface.detected = true;
|
|
945
|
+
envSurface.secretNameRefs.push(databaseUrlRef);
|
|
946
|
+
if (clientFile) {
|
|
947
|
+
pushBlockFinding(report, {
|
|
948
|
+
id: `database-url-client-${file.relPath}`,
|
|
949
|
+
category: "env",
|
|
950
|
+
title: "DATABASE_URL is referenced from client-facing code",
|
|
951
|
+
evidenceRef: databaseUrlRef,
|
|
952
|
+
reasonCodes: ["env_reference_detected", "database_url_client_exposure", "blocked_data_exposure", "safe_next_action_available"],
|
|
953
|
+
safeNextAction: "Remove DATABASE_URL from client-facing code and keep database access behind a server-only path.",
|
|
954
|
+
});
|
|
955
|
+
envSurface.clientExposureSignals.push(databaseUrlRef);
|
|
956
|
+
envSurface.risk = pickHigherRisk(envSurface.risk, "blocked");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (clientFile) {
|
|
961
|
+
const importRef = dbClientImportRef(file, text);
|
|
962
|
+
if (importRef) {
|
|
963
|
+
pushBlockFinding(report, {
|
|
964
|
+
id: `client-db-import-${file.relPath}`,
|
|
965
|
+
category: "service_role",
|
|
966
|
+
title: "Client-facing code imports a server or database client",
|
|
967
|
+
evidenceRef: importRef,
|
|
968
|
+
reasonCodes: ["blocked_data_exposure", "production_data_risk", "safe_next_action_available"],
|
|
969
|
+
safeNextAction: "Move database or server-only imports out of client-facing code before shipping.",
|
|
970
|
+
});
|
|
971
|
+
if (/@prisma\/client|prisma/i.test(importRef)) prismaSurface.clientExposureRefs.push(importRef);
|
|
972
|
+
if (/drizzle/i.test(importRef)) drizzleSurface.migrationRefs.push(importRef);
|
|
973
|
+
envSurface.clientExposureSignals.push(importRef);
|
|
974
|
+
envSurface.risk = pickHigherRisk(envSurface.risk, "blocked");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (/storage\.(from|getBucket|createBucket|updateBucket)|createClient/i.test(text) && /storage/i.test(text)) {
|
|
979
|
+
storageSurface.detected = true;
|
|
980
|
+
storageSurface.bucketRefs.push(routeRef);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const publicBucketRefs = collectPatternRefs(file, /\b(public|anon)\b[\s\S]{0,80}\b(bucket|storage\.from|getBucket|createBucket|updateBucket)\b|\b(bucket|storage\.buckets)\b[\s\S]{0,120}\bpublic\b/ig, { limit: 2 });
|
|
984
|
+
for (const ref of publicBucketRefs) {
|
|
985
|
+
const label = String(ref || "");
|
|
986
|
+
const severity = SENSITIVE_BUCKET_RE.test(text) ? "block" : LOWER_RISK_BUCKET_RE.test(text) ? "warn" : "warn";
|
|
987
|
+
addFinding(report, {
|
|
988
|
+
id: `storage-public-${ref}`,
|
|
989
|
+
severity,
|
|
990
|
+
category: "storage",
|
|
991
|
+
title: severity === "block"
|
|
992
|
+
? "Sensitive storage bucket appears public"
|
|
993
|
+
: "Public storage bucket needs review",
|
|
994
|
+
evidenceRef: ref,
|
|
995
|
+
reasonCodes: [
|
|
996
|
+
"storage_detected",
|
|
997
|
+
"storage_public_bucket_detected",
|
|
998
|
+
...(SENSITIVE_BUCKET_RE.test(text) ? ["sensitive_storage_public_risk", "production_data_risk"] : ["not_enough_evidence"]),
|
|
999
|
+
"safe_next_action_available",
|
|
1000
|
+
],
|
|
1001
|
+
safeNextAction: severity === "block"
|
|
1002
|
+
? "Restrict the sensitive bucket or move the files behind a narrower policy before shipping."
|
|
1003
|
+
: "Review whether the bucket should stay public and keep sensitive files out of that path.",
|
|
1004
|
+
});
|
|
1005
|
+
storageSurface.publicAccessSignals.push(ref);
|
|
1006
|
+
storageSurface.risk = pickHigherRisk(storageSurface.risk, severity === "block" ? "blocked" : "medium");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
envSurface.secretNameRefs = unique(envSurface.secretNameRefs).slice(0, 8);
|
|
1011
|
+
envSurface.clientExposureSignals = unique(envSurface.clientExposureSignals).slice(0, 8);
|
|
1012
|
+
prismaSurface.clientExposureRefs = unique(prismaSurface.clientExposureRefs).slice(0, 8);
|
|
1013
|
+
apiSurface.routeRefs = unique(apiSurface.routeRefs).slice(0, 8);
|
|
1014
|
+
apiSurface.authEvidence = unique(apiSurface.authEvidence).slice(0, 8);
|
|
1015
|
+
apiSurface.missingAuthSignals = unique(apiSurface.missingAuthSignals).slice(0, 8);
|
|
1016
|
+
storageSurface.bucketRefs = unique(storageSurface.bucketRefs).slice(0, 8);
|
|
1017
|
+
storageSurface.publicAccessSignals = unique(storageSurface.publicAccessSignals).slice(0, 8);
|
|
1018
|
+
|
|
1019
|
+
if (storageSurface.detected && storageSurface.publicAccessSignals.length === 0 && storageSurface.bucketRefs.length > 0) {
|
|
1020
|
+
storageSurface.risk = pickHigherRisk(storageSurface.risk, "low");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (apiSurface.detected && apiSurface.risk === "none") {
|
|
1024
|
+
apiSurface.risk = apiSurface.authEvidence.length > 0 ? "low" : "medium";
|
|
1025
|
+
}
|
|
1026
|
+
if (supabaseSurface.detected && supabaseSurface.risk === "none") {
|
|
1027
|
+
supabaseSurface.risk = supabaseSurface.rlsEvidence.length > 0 || supabaseSurface.policyEvidence.length > 0 ? "low" : "medium";
|
|
1028
|
+
}
|
|
1029
|
+
if (postgresSurface.detected && postgresSurface.risk === "none") {
|
|
1030
|
+
postgresSurface.risk = postgresSurface.grants.length > 0 ? "medium" : "unknown";
|
|
1031
|
+
}
|
|
1032
|
+
if (prismaSurface.detected && prismaSurface.risk === "none") {
|
|
1033
|
+
prismaSurface.risk = "medium";
|
|
1034
|
+
}
|
|
1035
|
+
if (drizzleSurface.detected && drizzleSurface.risk === "none") {
|
|
1036
|
+
drizzleSurface.risk = "medium";
|
|
1037
|
+
}
|
|
1038
|
+
if (envSurface.detected && envSurface.risk === "none") {
|
|
1039
|
+
envSurface.risk = envSurface.secretNameRefs.length > 0 ? "low" : "unknown";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
report.findings.sort((left, right) => severityRank(right.severity) - severityRank(left.severity));
|
|
1043
|
+
report.findings = report.findings.filter((finding, index, list) => {
|
|
1044
|
+
const key = `${finding.severity}|${finding.category}|${finding.title}|${finding.evidenceRef || ""}`;
|
|
1045
|
+
return list.findIndex((item) => `${item.severity}|${item.category}|${item.title}|${item.evidenceRef || ""}` === key) === index;
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
const blockers = report.findings.filter((finding) => finding.severity === "block");
|
|
1049
|
+
const warns = report.findings.filter((finding) => finding.severity === "warn");
|
|
1050
|
+
const infos = report.findings.filter((finding) => finding.severity === "info");
|
|
1051
|
+
const detectedSurfaces = Object.values(report.surfaces).filter((surface) => surface.detected).length;
|
|
1052
|
+
|
|
1053
|
+
report.rollup.readySignals = infos.map((finding) => finding.title).slice(0, 6);
|
|
1054
|
+
report.rollup.needsReview = warns.map((finding) => finding.title).slice(0, 6);
|
|
1055
|
+
report.rollup.blockedItems = blockers.map((finding) => finding.title).slice(0, 6);
|
|
1056
|
+
report.rollup.notEnoughEvidence = report.findings
|
|
1057
|
+
.filter((finding) => finding.reasonCodes.includes("not_enough_evidence") || finding.reasonCodes.includes("rls_missing_or_unclear"))
|
|
1058
|
+
.map((finding) => finding.title)
|
|
1059
|
+
.slice(0, 6);
|
|
1060
|
+
|
|
1061
|
+
let safeNextAction = "Keep the local data boundary evidence current as the repo changes.";
|
|
1062
|
+
if (blockers[0]?.safeNextAction) {
|
|
1063
|
+
safeNextAction = blockers[0].safeNextAction;
|
|
1064
|
+
} else if (warns[0]?.safeNextAction) {
|
|
1065
|
+
safeNextAction = warns[0].safeNextAction;
|
|
1066
|
+
} else if (infos[0]?.safeNextAction) {
|
|
1067
|
+
safeNextAction = infos[0].safeNextAction;
|
|
1068
|
+
} else if (detectedSurfaces === 0) {
|
|
1069
|
+
safeNextAction = "No local data surface was detected. Re-run after adding database, storage, or API code.";
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (detectedSurfaces === 0) {
|
|
1073
|
+
report.status = "no_data";
|
|
1074
|
+
} else if (blockers.length > 0) {
|
|
1075
|
+
report.status = "blocked";
|
|
1076
|
+
} else if (warns.length > 0) {
|
|
1077
|
+
report.status = "warn";
|
|
1078
|
+
} else {
|
|
1079
|
+
report.status = "pass";
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
report.generatedAt = nowIso();
|
|
1083
|
+
report.summary = {
|
|
1084
|
+
surfacesInspected: detectedSurfaces,
|
|
1085
|
+
risksFound: report.findings.length,
|
|
1086
|
+
blockers: blockers.length,
|
|
1087
|
+
needsReview: warns.length,
|
|
1088
|
+
safeNextAction,
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
if (options.write === true) {
|
|
1092
|
+
ensureCcoDirs(cwd);
|
|
1093
|
+
safeWriteJson(cwd, REPORT_JSON_REL, report);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return report;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function generateDataPermissionsGuard(cwd = process.cwd(), options = {}) {
|
|
1100
|
+
const report = buildDataPermissionsGuard(cwd, { ...options, write: options.write !== false });
|
|
1101
|
+
return {
|
|
1102
|
+
report,
|
|
1103
|
+
reportPath: path.join(cwd, REPORT_JSON_REL),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function loadDataPermissionsGuard(cwd = process.cwd()) {
|
|
1108
|
+
return safeReadJson(cwd, REPORT_JSON_REL, null);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function formatDataPermissionsGuardText(report) {
|
|
1112
|
+
const lines = [
|
|
1113
|
+
"Avorelo Data & Permissions Guard",
|
|
1114
|
+
`Status: ${String(report.status || "no_data").toUpperCase()} · redacted=${report.redacted === true ? "yes" : "no"}`,
|
|
1115
|
+
`Surfaces: ${report.summary?.surfacesInspected || 0} · risks=${report.summary?.risksFound || 0} · blockers=${report.summary?.blockers || 0} · review=${report.summary?.needsReview || 0}`,
|
|
1116
|
+
`Safe next action: ${report.summary?.safeNextAction || "Review the local data boundary before shipping."}`,
|
|
1117
|
+
"",
|
|
1118
|
+
"Findings:",
|
|
1119
|
+
];
|
|
1120
|
+
|
|
1121
|
+
const topFindings = (report.findings || []).slice(0, 6);
|
|
1122
|
+
if (topFindings.length === 0) {
|
|
1123
|
+
lines.push(report.summary?.surfacesInspected
|
|
1124
|
+
? "- No risky local data-permission findings were detected."
|
|
1125
|
+
: "- No local data surface was detected.");
|
|
1126
|
+
} else {
|
|
1127
|
+
for (const finding of topFindings) {
|
|
1128
|
+
lines.push(`- [${finding.severity}] ${finding.title}${finding.evidenceRef ? ` · ${finding.evidenceRef}` : ""}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return lines.join("\n");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function formatDataPermissionsGuardStatusLineFromReport(report) {
|
|
1136
|
+
return `Data guard: ${String(report.status || "no_data").toUpperCase()}`
|
|
1137
|
+
+ ` · surfaces=${report.summary?.surfacesInspected || 0}`
|
|
1138
|
+
+ ` · blockers=${report.summary?.blockers || 0}`
|
|
1139
|
+
+ ` · next=${report.summary?.safeNextAction || "review local data evidence"}`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function buildDataPermissionsGuardSurface(cwd = process.cwd(), options = {}) {
|
|
1143
|
+
const report = loadDataPermissionsGuard(cwd) || (options.buildIfMissing ? buildDataPermissionsGuard(cwd, { write: false }) : null);
|
|
1144
|
+
if (!report) {
|
|
1145
|
+
return {
|
|
1146
|
+
status: "not_run",
|
|
1147
|
+
blockerCount: 0,
|
|
1148
|
+
needsReviewCount: 0,
|
|
1149
|
+
surfacesInspected: 0,
|
|
1150
|
+
nextBestAction: "Run `avorelo data-guard --text` before treating a local data boundary as safe.",
|
|
1151
|
+
statusLine: "Data guard: NOT RUN · run `avorelo data-guard --text`",
|
|
1152
|
+
showInStatus: true,
|
|
1153
|
+
showInDashboard: true,
|
|
1154
|
+
evidenceRef: null,
|
|
1155
|
+
limitations: [
|
|
1156
|
+
"Local static scan only. No live database connection or cloud scanner.",
|
|
1157
|
+
],
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
status: report.status,
|
|
1163
|
+
blockerCount: report.summary?.blockers || 0,
|
|
1164
|
+
needsReviewCount: report.summary?.needsReview || 0,
|
|
1165
|
+
surfacesInspected: report.summary?.surfacesInspected || 0,
|
|
1166
|
+
nextBestAction: report.summary?.safeNextAction,
|
|
1167
|
+
statusLine: formatDataPermissionsGuardStatusLineFromReport(report),
|
|
1168
|
+
showInStatus: true,
|
|
1169
|
+
showInDashboard: true,
|
|
1170
|
+
evidenceRef: REPORT_JSON_REL,
|
|
1171
|
+
limitations: [
|
|
1172
|
+
"Local static scan only. No live database connection or cloud scanner.",
|
|
1173
|
+
"Absence of evidence stays bounded and reason-coded.",
|
|
1174
|
+
],
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
module.exports = {
|
|
1179
|
+
CONTRACT,
|
|
1180
|
+
REPORT_JSON_REL,
|
|
1181
|
+
buildDataPermissionsGuard,
|
|
1182
|
+
generateDataPermissionsGuard,
|
|
1183
|
+
loadDataPermissionsGuard,
|
|
1184
|
+
formatDataPermissionsGuardText,
|
|
1185
|
+
buildDataPermissionsGuardSurface,
|
|
1186
|
+
formatDataPermissionsGuardStatusLineFromReport,
|
|
1187
|
+
};
|