@m2-ai/lattice-cli 0.0.0-stage → 0.8.7
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/DISCLOSURE +1 -0
- package/README.md +118 -2
- package/bundles/expo-mobile/.agents/skills/lattice-feature-handoff/SKILL.md +31 -0
- package/bundles/expo-mobile/.agents/skills/lattice-release-manager/SKILL.md +29 -0
- package/bundles/expo-mobile/.agents/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/expo-mobile/.claude/agents/lattice-feature-agent.md +10 -0
- package/bundles/expo-mobile/.claude/agents/lattice-release-manager.md +9 -0
- package/bundles/expo-mobile/.claude/hooks/README.md +9 -0
- package/bundles/expo-mobile/.claude/hooks/pre-commit-verify.mjs +10 -0
- package/bundles/expo-mobile/.claude/hooks/pre-tool-use.mjs +51 -0
- package/bundles/expo-mobile/.claude/rules/lattice-core.md +20 -0
- package/bundles/expo-mobile/.claude/settings.json +38 -0
- package/bundles/expo-mobile/.claude/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/expo-mobile/.codex/agents/lattice-feature-agent.toml +9 -0
- package/bundles/expo-mobile/.codex/agents/lattice-release-manager.toml +9 -0
- package/bundles/expo-mobile/.cursor/mcp.json +34 -0
- package/bundles/expo-mobile/.cursor/rules/lattice-core.mdc +35 -0
- package/bundles/expo-mobile/.cursor/rules.md +94 -0
- package/bundles/expo-mobile/.env.example +19 -0
- package/bundles/expo-mobile/.github/PULL_REQUEST_TEMPLATE.md +42 -0
- package/bundles/expo-mobile/.github/agents/lattice-feature-agent.agent.md +7 -0
- package/bundles/expo-mobile/.github/agents/lattice-release-manager.agent.md +7 -0
- package/bundles/expo-mobile/.github/copilot-instructions.md +37 -0
- package/bundles/expo-mobile/.github/instructions/lattice-core.instructions.md +11 -0
- package/bundles/expo-mobile/.github/workflows/ci.yml +26 -0
- package/bundles/expo-mobile/.gitmessage +24 -0
- package/bundles/expo-mobile/.lattice/config.json +9 -0
- package/bundles/expo-mobile/.lattice/dependency-security.json +91 -0
- package/bundles/expo-mobile/.lattice/prompts/01-feature.md +36 -0
- package/bundles/expo-mobile/.lattice/prompts/02-bugfix.md +32 -0
- package/bundles/expo-mobile/.lattice/prompts/03-refactor.md +30 -0
- package/bundles/expo-mobile/.lattice/prompts/04-test.md +31 -0
- package/bundles/expo-mobile/.lattice/prompts/05-polish.md +38 -0
- package/bundles/expo-mobile/.lattice/prompts/06-security.md +32 -0
- package/bundles/expo-mobile/.lattice/release-workflow.json +55 -0
- package/bundles/expo-mobile/.lattice/schemas/feature-handoff.schema.json +264 -0
- package/bundles/expo-mobile/.lattice/templates/feature-handoff.json +52 -0
- package/bundles/expo-mobile/.mcp.json +35 -0
- package/bundles/expo-mobile/AGENTS.md +58 -0
- package/bundles/expo-mobile/CLAUDE.md +75 -0
- package/bundles/expo-mobile/PROJECT_GUIDE.md +299 -0
- package/bundles/expo-mobile/README.md +39 -0
- package/bundles/expo-mobile/__tests__/home.test.tsx +22 -0
- package/bundles/expo-mobile/app/_layout.tsx +20 -0
- package/bundles/expo-mobile/app/index.tsx +72 -0
- package/bundles/expo-mobile/app.json +33 -0
- package/bundles/expo-mobile/components/ui/Button.tsx +75 -0
- package/bundles/expo-mobile/components/ui/Input.tsx +50 -0
- package/bundles/expo-mobile/components/ui/index.ts +2 -0
- package/bundles/expo-mobile/docs/LATTICE_RELEASE_WORKFLOW.md +53 -0
- package/bundles/expo-mobile/docs/rules/codex.md +62 -0
- package/bundles/expo-mobile/eslint.config.js +7 -0
- package/bundles/expo-mobile/lib/supabase/client.ts +22 -0
- package/bundles/expo-mobile/lib/supabase/realtime.ts +10 -0
- package/bundles/expo-mobile/lib/supabase/storage.ts +6 -0
- package/bundles/expo-mobile/package.json +65 -0
- package/bundles/expo-mobile/plugins/with-lattice-commons-io.cjs +42 -0
- package/bundles/expo-mobile/prompts/01-feature-implementation.md +48 -0
- package/bundles/expo-mobile/prompts/02-bug-fix.md +43 -0
- package/bundles/expo-mobile/prompts/03-safe-refactor.md +38 -0
- package/bundles/expo-mobile/prompts/04-test-first.md +41 -0
- package/bundles/expo-mobile/prompts/05-ui-polish.md +44 -0
- package/bundles/expo-mobile/prompts/06-security-review.md +39 -0
- package/bundles/expo-mobile/prompts/quality.md +143 -0
- package/bundles/expo-mobile/supabase/migrations/001_initial_schema.sql +66 -0
- package/bundles/expo-mobile/tsconfig.json +21 -0
- package/bundles/expo-mobile/types/database.types.ts +76 -0
- package/bundles/expo-mobile/vendor/m2ai-braces-3.0.3-m2ai.1.tar +0 -0
- package/bundles/expo-supabase/.agents/skills/lattice-feature-handoff/SKILL.md +31 -0
- package/bundles/expo-supabase/.agents/skills/lattice-release-manager/SKILL.md +29 -0
- package/bundles/expo-supabase/.agents/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/expo-supabase/.claude/agents/lattice-feature-agent.md +10 -0
- package/bundles/expo-supabase/.claude/agents/lattice-release-manager.md +9 -0
- package/bundles/expo-supabase/.claude/hooks/README.md +9 -0
- package/bundles/expo-supabase/.claude/hooks/pre-commit-verify.mjs +10 -0
- package/bundles/expo-supabase/.claude/hooks/pre-tool-use.mjs +51 -0
- package/bundles/expo-supabase/.claude/rules/lattice-core.md +20 -0
- package/bundles/expo-supabase/.claude/settings.json +38 -0
- package/bundles/expo-supabase/.claude/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/expo-supabase/.codex/agents/lattice-feature-agent.toml +9 -0
- package/bundles/expo-supabase/.codex/agents/lattice-release-manager.toml +9 -0
- package/bundles/expo-supabase/.cursor/mcp.json +34 -0
- package/bundles/expo-supabase/.cursor/rules/lattice-core.mdc +35 -0
- package/bundles/expo-supabase/.cursor/rules.md +94 -0
- package/bundles/expo-supabase/.env.example +12 -0
- package/bundles/expo-supabase/.github/PULL_REQUEST_TEMPLATE.md +42 -0
- package/bundles/expo-supabase/.github/agents/lattice-feature-agent.agent.md +7 -0
- package/bundles/expo-supabase/.github/agents/lattice-release-manager.agent.md +7 -0
- package/bundles/expo-supabase/.github/copilot-instructions.md +37 -0
- package/bundles/expo-supabase/.github/instructions/lattice-core.instructions.md +11 -0
- package/bundles/expo-supabase/.github/workflows/ci.yml +26 -0
- package/bundles/expo-supabase/.gitmessage +24 -0
- package/bundles/expo-supabase/.lattice/config.json +9 -0
- package/bundles/expo-supabase/.lattice/dependency-security.json +91 -0
- package/bundles/expo-supabase/.lattice/prompts/01-feature.md +36 -0
- package/bundles/expo-supabase/.lattice/prompts/02-bugfix.md +32 -0
- package/bundles/expo-supabase/.lattice/prompts/03-refactor.md +30 -0
- package/bundles/expo-supabase/.lattice/prompts/04-test.md +31 -0
- package/bundles/expo-supabase/.lattice/prompts/05-polish.md +38 -0
- package/bundles/expo-supabase/.lattice/prompts/06-security.md +32 -0
- package/bundles/expo-supabase/.lattice/release-workflow.json +55 -0
- package/bundles/expo-supabase/.lattice/schemas/feature-handoff.schema.json +264 -0
- package/bundles/expo-supabase/.lattice/templates/feature-handoff.json +52 -0
- package/bundles/expo-supabase/.mcp.json +35 -0
- package/bundles/expo-supabase/AGENTS.md +58 -0
- package/bundles/expo-supabase/CLAUDE.md +75 -0
- package/bundles/expo-supabase/PROJECT_GUIDE.md +201 -0
- package/bundles/expo-supabase/README.md +38 -0
- package/bundles/expo-supabase/__tests__/home.test.tsx +22 -0
- package/bundles/expo-supabase/app/_layout.tsx +14 -0
- package/bundles/expo-supabase/app/index.tsx +72 -0
- package/bundles/expo-supabase/app.json +33 -0
- package/bundles/expo-supabase/components/ui/Button.tsx +75 -0
- package/bundles/expo-supabase/components/ui/Input.tsx +50 -0
- package/bundles/expo-supabase/components/ui/index.ts +2 -0
- package/bundles/expo-supabase/docs/LATTICE_RELEASE_WORKFLOW.md +53 -0
- package/bundles/expo-supabase/docs/rules/codex.md +62 -0
- package/bundles/expo-supabase/eslint.config.js +7 -0
- package/bundles/expo-supabase/lib/supabase/client.ts +22 -0
- package/bundles/expo-supabase/lib/supabase/realtime.ts +10 -0
- package/bundles/expo-supabase/lib/supabase/storage.ts +6 -0
- package/bundles/expo-supabase/package.json +60 -0
- package/bundles/expo-supabase/plugins/with-lattice-commons-io.cjs +42 -0
- package/bundles/expo-supabase/prompts/01-feature-implementation.md +8 -0
- package/bundles/expo-supabase/prompts/02-bug-fix.md +9 -0
- package/bundles/expo-supabase/prompts/03-safe-refactor.md +8 -0
- package/bundles/expo-supabase/prompts/04-test-first.md +7 -0
- package/bundles/expo-supabase/prompts/05-ui-polish.md +7 -0
- package/bundles/expo-supabase/prompts/06-security-review.md +7 -0
- package/bundles/expo-supabase/prompts/quality.md +143 -0
- package/bundles/expo-supabase/supabase/migrations/001_initial_schema.sql +66 -0
- package/bundles/expo-supabase/tsconfig.json +21 -0
- package/bundles/expo-supabase/types/database.types.ts +76 -0
- package/bundles/expo-supabase/vendor/m2ai-braces-3.0.3-m2ai.1.tar +0 -0
- package/bundles/nextjs-landing/.agents/skills/lattice-feature-handoff/SKILL.md +31 -0
- package/bundles/nextjs-landing/.agents/skills/lattice-release-manager/SKILL.md +29 -0
- package/bundles/nextjs-landing/.agents/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-landing/.claude/agents/lattice-feature-agent.md +10 -0
- package/bundles/nextjs-landing/.claude/agents/lattice-release-manager.md +9 -0
- package/bundles/nextjs-landing/.claude/hooks/README.md +9 -0
- package/bundles/nextjs-landing/.claude/hooks/pre-commit-verify.mjs +10 -0
- package/bundles/nextjs-landing/.claude/hooks/pre-tool-use.mjs +51 -0
- package/bundles/nextjs-landing/.claude/rules/lattice-core.md +20 -0
- package/bundles/nextjs-landing/.claude/settings.json +38 -0
- package/bundles/nextjs-landing/.claude/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-landing/.codex/agents/lattice-feature-agent.toml +9 -0
- package/bundles/nextjs-landing/.codex/agents/lattice-release-manager.toml +9 -0
- package/bundles/nextjs-landing/.cursor/mcp.json +34 -0
- package/bundles/nextjs-landing/.cursor/rules/lattice-core.mdc +35 -0
- package/bundles/nextjs-landing/.cursor/rules.md +95 -0
- package/bundles/nextjs-landing/.env.example +24 -0
- package/bundles/nextjs-landing/.github/PULL_REQUEST_TEMPLATE.md +42 -0
- package/bundles/nextjs-landing/.github/agents/lattice-feature-agent.agent.md +7 -0
- package/bundles/nextjs-landing/.github/agents/lattice-release-manager.agent.md +7 -0
- package/bundles/nextjs-landing/.github/copilot-instructions.md +37 -0
- package/bundles/nextjs-landing/.github/instructions/lattice-core.instructions.md +11 -0
- package/bundles/nextjs-landing/.github/workflows/ci.yml +26 -0
- package/bundles/nextjs-landing/.gitmessage +24 -0
- package/bundles/nextjs-landing/.lattice/config.json +9 -0
- package/bundles/nextjs-landing/.lattice/dependency-security.json +46 -0
- package/bundles/nextjs-landing/.lattice/prompts/01-feature.md +36 -0
- package/bundles/nextjs-landing/.lattice/prompts/02-bugfix.md +32 -0
- package/bundles/nextjs-landing/.lattice/prompts/03-refactor.md +30 -0
- package/bundles/nextjs-landing/.lattice/prompts/04-test.md +31 -0
- package/bundles/nextjs-landing/.lattice/prompts/05-polish.md +38 -0
- package/bundles/nextjs-landing/.lattice/prompts/06-security.md +32 -0
- package/bundles/nextjs-landing/.lattice/release-workflow.json +55 -0
- package/bundles/nextjs-landing/.lattice/schemas/feature-handoff.schema.json +264 -0
- package/bundles/nextjs-landing/.lattice/templates/feature-handoff.json +52 -0
- package/bundles/nextjs-landing/.mcp.json +35 -0
- package/bundles/nextjs-landing/AGENTS.md +58 -0
- package/bundles/nextjs-landing/CLAUDE.md +76 -0
- package/bundles/nextjs-landing/PROJECT_GUIDE.md +216 -0
- package/bundles/nextjs-landing/README.md +39 -0
- package/bundles/nextjs-landing/app/api/example/[id]/route.ts +34 -0
- package/bundles/nextjs-landing/app/api/example/route.ts +24 -0
- package/bundles/nextjs-landing/app/api/health/route.ts +8 -0
- package/bundles/nextjs-landing/app/global-error.tsx +17 -0
- package/bundles/nextjs-landing/app/globals.css +55 -0
- package/bundles/nextjs-landing/app/layout.tsx +22 -0
- package/bundles/nextjs-landing/app/page.tsx +42 -0
- package/bundles/nextjs-landing/components/ui/button.tsx +39 -0
- package/bundles/nextjs-landing/components/ui/card.tsx +53 -0
- package/bundles/nextjs-landing/components/ui/index.ts +4 -0
- package/bundles/nextjs-landing/components/ui/input.tsx +23 -0
- package/bundles/nextjs-landing/components/ui/spinner.tsx +22 -0
- package/bundles/nextjs-landing/docs/LATTICE_RELEASE_WORKFLOW.md +53 -0
- package/bundles/nextjs-landing/docs/rules/codex.md +62 -0
- package/bundles/nextjs-landing/eslint.config.mjs +16 -0
- package/bundles/nextjs-landing/instrumentation-client.ts +30 -0
- package/bundles/nextjs-landing/instrumentation.ts +12 -0
- package/bundles/nextjs-landing/jest.config.mjs +16 -0
- package/bundles/nextjs-landing/jest.setup.js +1 -0
- package/bundles/nextjs-landing/lib/utils.ts +6 -0
- package/bundles/nextjs-landing/next-env.d.ts +5 -0
- package/bundles/nextjs-landing/next.config.ts +15 -0
- package/bundles/nextjs-landing/package.json +48 -0
- package/bundles/nextjs-landing/postcss.config.mjs +7 -0
- package/bundles/nextjs-landing/prompts/01-feature-implementation.md +9 -0
- package/bundles/nextjs-landing/prompts/02-bug-fix.md +10 -0
- package/bundles/nextjs-landing/prompts/03-safe-refactor.md +8 -0
- package/bundles/nextjs-landing/prompts/04-test-first.md +8 -0
- package/bundles/nextjs-landing/prompts/05-ui-polish.md +8 -0
- package/bundles/nextjs-landing/prompts/06-security-review.md +7 -0
- package/bundles/nextjs-landing/prompts/quality.md +269 -0
- package/bundles/nextjs-landing/sentry.edge.config.ts +15 -0
- package/bundles/nextjs-landing/sentry.server.config.ts +15 -0
- package/bundles/nextjs-landing/tsconfig.json +42 -0
- package/bundles/nextjs-landing/vendor/m2ai-braces-3.0.3-m2ai.1.tar +0 -0
- package/bundles/nextjs-saas/.agents/skills/lattice-feature-handoff/SKILL.md +31 -0
- package/bundles/nextjs-saas/.agents/skills/lattice-release-manager/SKILL.md +29 -0
- package/bundles/nextjs-saas/.agents/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-saas/.claude/agents/lattice-feature-agent.md +10 -0
- package/bundles/nextjs-saas/.claude/agents/lattice-release-manager.md +9 -0
- package/bundles/nextjs-saas/.claude/hooks/README.md +9 -0
- package/bundles/nextjs-saas/.claude/hooks/pre-commit-verify.mjs +10 -0
- package/bundles/nextjs-saas/.claude/hooks/pre-tool-use.mjs +51 -0
- package/bundles/nextjs-saas/.claude/rules/lattice-core.md +20 -0
- package/bundles/nextjs-saas/.claude/settings.json +38 -0
- package/bundles/nextjs-saas/.claude/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-saas/.codex/agents/lattice-feature-agent.toml +9 -0
- package/bundles/nextjs-saas/.codex/agents/lattice-release-manager.toml +9 -0
- package/bundles/nextjs-saas/.cursor/mcp.json +34 -0
- package/bundles/nextjs-saas/.cursor/rules/lattice-core.mdc +35 -0
- package/bundles/nextjs-saas/.cursor/rules.md +95 -0
- package/bundles/nextjs-saas/.env.example +48 -0
- package/bundles/nextjs-saas/.github/PULL_REQUEST_TEMPLATE.md +42 -0
- package/bundles/nextjs-saas/.github/agents/lattice-feature-agent.agent.md +7 -0
- package/bundles/nextjs-saas/.github/agents/lattice-release-manager.agent.md +7 -0
- package/bundles/nextjs-saas/.github/copilot-instructions.md +37 -0
- package/bundles/nextjs-saas/.github/instructions/lattice-core.instructions.md +11 -0
- package/bundles/nextjs-saas/.github/workflows/ci.yml +26 -0
- package/bundles/nextjs-saas/.gitmessage +24 -0
- package/bundles/nextjs-saas/.lattice/config.json +9 -0
- package/bundles/nextjs-saas/.lattice/dependency-security.json +46 -0
- package/bundles/nextjs-saas/.lattice/prompts/01-feature.md +36 -0
- package/bundles/nextjs-saas/.lattice/prompts/02-bugfix.md +32 -0
- package/bundles/nextjs-saas/.lattice/prompts/03-refactor.md +30 -0
- package/bundles/nextjs-saas/.lattice/prompts/04-test.md +31 -0
- package/bundles/nextjs-saas/.lattice/prompts/05-polish.md +38 -0
- package/bundles/nextjs-saas/.lattice/prompts/06-security.md +32 -0
- package/bundles/nextjs-saas/.lattice/prompts/quality.md +269 -0
- package/bundles/nextjs-saas/.lattice/release-workflow.json +55 -0
- package/bundles/nextjs-saas/.lattice/schemas/feature-handoff.schema.json +264 -0
- package/bundles/nextjs-saas/.lattice/templates/feature-handoff.json +52 -0
- package/bundles/nextjs-saas/.mcp.json +35 -0
- package/bundles/nextjs-saas/AGENTS.md +58 -0
- package/bundles/nextjs-saas/CLAUDE.md +76 -0
- package/bundles/nextjs-saas/PROJECT_GUIDE.md +279 -0
- package/bundles/nextjs-saas/README.md +42 -0
- package/bundles/nextjs-saas/app/(auth)/forgot-password/page.tsx +101 -0
- package/bundles/nextjs-saas/app/(auth)/layout.tsx +7 -0
- package/bundles/nextjs-saas/app/(auth)/login/page.tsx +108 -0
- package/bundles/nextjs-saas/app/(auth)/reset-password/page.tsx +135 -0
- package/bundles/nextjs-saas/app/(auth)/signup/page.tsx +144 -0
- package/bundles/nextjs-saas/app/(dashboard)/dashboard/page.tsx +80 -0
- package/bundles/nextjs-saas/app/(dashboard)/layout.tsx +49 -0
- package/bundles/nextjs-saas/app/api/billing/checkout/route.ts +47 -0
- package/bundles/nextjs-saas/app/api/billing/portal/route.ts +29 -0
- package/bundles/nextjs-saas/app/api/example/[id]/route.ts +93 -0
- package/bundles/nextjs-saas/app/api/example/route.ts +61 -0
- package/bundles/nextjs-saas/app/api/health/route.ts +8 -0
- package/bundles/nextjs-saas/app/api/stripe/webhook/route.ts +57 -0
- package/bundles/nextjs-saas/app/api/user/route.ts +49 -0
- package/bundles/nextjs-saas/app/auth/callback/route.ts +16 -0
- package/bundles/nextjs-saas/app/auth/signout/route.ts +10 -0
- package/bundles/nextjs-saas/app/global-error.tsx +17 -0
- package/bundles/nextjs-saas/app/globals.css +55 -0
- package/bundles/nextjs-saas/app/layout.tsx +22 -0
- package/bundles/nextjs-saas/app/page.tsx +84 -0
- package/bundles/nextjs-saas/components/ui/button.tsx +39 -0
- package/bundles/nextjs-saas/components/ui/card.tsx +53 -0
- package/bundles/nextjs-saas/components/ui/index.ts +4 -0
- package/bundles/nextjs-saas/components/ui/input.tsx +23 -0
- package/bundles/nextjs-saas/components/ui/spinner.tsx +22 -0
- package/bundles/nextjs-saas/docs/LATTICE_RELEASE_WORKFLOW.md +53 -0
- package/bundles/nextjs-saas/docs/rules/codex.md +62 -0
- package/bundles/nextjs-saas/eslint.config.mjs +16 -0
- package/bundles/nextjs-saas/instrumentation-client.ts +30 -0
- package/bundles/nextjs-saas/instrumentation.ts +12 -0
- package/bundles/nextjs-saas/jest.config.mjs +16 -0
- package/bundles/nextjs-saas/jest.setup.js +1 -0
- package/bundles/nextjs-saas/lib/auth/password-recovery.ts +94 -0
- package/bundles/nextjs-saas/lib/stripe.ts +10 -0
- package/bundles/nextjs-saas/lib/supabase/admin.ts +14 -0
- package/bundles/nextjs-saas/lib/supabase/client.ts +8 -0
- package/bundles/nextjs-saas/lib/supabase/server.ts +27 -0
- package/bundles/nextjs-saas/lib/utils.ts +6 -0
- package/bundles/nextjs-saas/next-env.d.ts +5 -0
- package/bundles/nextjs-saas/next.config.ts +15 -0
- package/bundles/nextjs-saas/package.json +50 -0
- package/bundles/nextjs-saas/postcss.config.mjs +7 -0
- package/bundles/nextjs-saas/prompts/01-feature-implementation.md +92 -0
- package/bundles/nextjs-saas/prompts/02-bug-fix.md +78 -0
- package/bundles/nextjs-saas/prompts/03-safe-refactor.md +82 -0
- package/bundles/nextjs-saas/prompts/04-test-first.md +80 -0
- package/bundles/nextjs-saas/prompts/05-ui-polish.md +82 -0
- package/bundles/nextjs-saas/prompts/06-security-review.md +74 -0
- package/bundles/nextjs-saas/proxy.ts +61 -0
- package/bundles/nextjs-saas/sentry.edge.config.ts +15 -0
- package/bundles/nextjs-saas/sentry.server.config.ts +15 -0
- package/bundles/nextjs-saas/supabase/migrations/001_initial_schema.sql +66 -0
- package/bundles/nextjs-saas/supabase/migrations/002_billing.sql +17 -0
- package/bundles/nextjs-saas/tsconfig.json +42 -0
- package/bundles/nextjs-saas/types/database.types.ts +76 -0
- package/bundles/nextjs-saas/vendor/m2ai-braces-3.0.3-m2ai.1.tar +0 -0
- package/bundles/nextjs-trpc/.agents/skills/lattice-feature-handoff/SKILL.md +31 -0
- package/bundles/nextjs-trpc/.agents/skills/lattice-release-manager/SKILL.md +29 -0
- package/bundles/nextjs-trpc/.agents/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-trpc/.claude/agents/lattice-feature-agent.md +10 -0
- package/bundles/nextjs-trpc/.claude/agents/lattice-release-manager.md +9 -0
- package/bundles/nextjs-trpc/.claude/hooks/README.md +9 -0
- package/bundles/nextjs-trpc/.claude/hooks/pre-commit-verify.mjs +10 -0
- package/bundles/nextjs-trpc/.claude/hooks/pre-tool-use.mjs +51 -0
- package/bundles/nextjs-trpc/.claude/rules/lattice-core.md +20 -0
- package/bundles/nextjs-trpc/.claude/settings.json +38 -0
- package/bundles/nextjs-trpc/.claude/skills/lattice-verify/SKILL.md +27 -0
- package/bundles/nextjs-trpc/.codex/agents/lattice-feature-agent.toml +9 -0
- package/bundles/nextjs-trpc/.codex/agents/lattice-release-manager.toml +9 -0
- package/bundles/nextjs-trpc/.cursor/mcp.json +34 -0
- package/bundles/nextjs-trpc/.cursor/rules/lattice-core.mdc +35 -0
- package/bundles/nextjs-trpc/.cursor/rules.md +95 -0
- package/bundles/nextjs-trpc/.env.example +21 -0
- package/bundles/nextjs-trpc/.github/PULL_REQUEST_TEMPLATE.md +42 -0
- package/bundles/nextjs-trpc/.github/agents/lattice-feature-agent.agent.md +7 -0
- package/bundles/nextjs-trpc/.github/agents/lattice-release-manager.agent.md +7 -0
- package/bundles/nextjs-trpc/.github/copilot-instructions.md +37 -0
- package/bundles/nextjs-trpc/.github/instructions/lattice-core.instructions.md +11 -0
- package/bundles/nextjs-trpc/.github/workflows/ci.yml +26 -0
- package/bundles/nextjs-trpc/.gitmessage +24 -0
- package/bundles/nextjs-trpc/.lattice/config.json +9 -0
- package/bundles/nextjs-trpc/.lattice/dependency-security.json +46 -0
- package/bundles/nextjs-trpc/.lattice/prompts/01-feature.md +36 -0
- package/bundles/nextjs-trpc/.lattice/prompts/02-bugfix.md +32 -0
- package/bundles/nextjs-trpc/.lattice/prompts/03-refactor.md +30 -0
- package/bundles/nextjs-trpc/.lattice/prompts/04-test.md +31 -0
- package/bundles/nextjs-trpc/.lattice/prompts/05-polish.md +38 -0
- package/bundles/nextjs-trpc/.lattice/prompts/06-security.md +32 -0
- package/bundles/nextjs-trpc/.lattice/release-workflow.json +55 -0
- package/bundles/nextjs-trpc/.lattice/schemas/feature-handoff.schema.json +264 -0
- package/bundles/nextjs-trpc/.lattice/templates/feature-handoff.json +52 -0
- package/bundles/nextjs-trpc/.mcp.json +35 -0
- package/bundles/nextjs-trpc/AGENTS.md +58 -0
- package/bundles/nextjs-trpc/CLAUDE.md +76 -0
- package/bundles/nextjs-trpc/PROJECT_GUIDE.md +187 -0
- package/bundles/nextjs-trpc/README.md +49 -0
- package/bundles/nextjs-trpc/app/api/trpc/[trpc]/route.ts +12 -0
- package/bundles/nextjs-trpc/app/global-error.tsx +17 -0
- package/bundles/nextjs-trpc/app/globals.css +55 -0
- package/bundles/nextjs-trpc/app/layout.tsx +19 -0
- package/bundles/nextjs-trpc/app/page.tsx +16 -0
- package/bundles/nextjs-trpc/app/providers.tsx +24 -0
- package/bundles/nextjs-trpc/components/ui/button.tsx +39 -0
- package/bundles/nextjs-trpc/components/ui/card.tsx +53 -0
- package/bundles/nextjs-trpc/components/ui/index.ts +4 -0
- package/bundles/nextjs-trpc/components/ui/input.tsx +23 -0
- package/bundles/nextjs-trpc/components/ui/spinner.tsx +22 -0
- package/bundles/nextjs-trpc/components/user-list.tsx +17 -0
- package/bundles/nextjs-trpc/docs/LATTICE_RELEASE_WORKFLOW.md +53 -0
- package/bundles/nextjs-trpc/docs/rules/codex.md +62 -0
- package/bundles/nextjs-trpc/eslint.config.mjs +16 -0
- package/bundles/nextjs-trpc/instrumentation-client.ts +20 -0
- package/bundles/nextjs-trpc/instrumentation.ts +12 -0
- package/bundles/nextjs-trpc/jest.config.mjs +16 -0
- package/bundles/nextjs-trpc/jest.setup.js +1 -0
- package/bundles/nextjs-trpc/lib/db.ts +17 -0
- package/bundles/nextjs-trpc/lib/trpc/react.tsx +7 -0
- package/bundles/nextjs-trpc/lib/utils.ts +6 -0
- package/bundles/nextjs-trpc/next-env.d.ts +5 -0
- package/bundles/nextjs-trpc/next.config.ts +15 -0
- package/bundles/nextjs-trpc/package.json +62 -0
- package/bundles/nextjs-trpc/postcss.config.mjs +7 -0
- package/bundles/nextjs-trpc/prisma/schema.prisma +41 -0
- package/bundles/nextjs-trpc/prisma.config.ts +10 -0
- package/bundles/nextjs-trpc/prompts/01-feature-implementation.md +9 -0
- package/bundles/nextjs-trpc/prompts/02-bug-fix.md +9 -0
- package/bundles/nextjs-trpc/prompts/03-safe-refactor.md +8 -0
- package/bundles/nextjs-trpc/prompts/04-test-first.md +8 -0
- package/bundles/nextjs-trpc/prompts/05-ui-polish.md +7 -0
- package/bundles/nextjs-trpc/prompts/06-security-review.md +7 -0
- package/bundles/nextjs-trpc/prompts/quality.md +269 -0
- package/bundles/nextjs-trpc/sentry.edge.config.ts +15 -0
- package/bundles/nextjs-trpc/sentry.server.config.ts +15 -0
- package/bundles/nextjs-trpc/server/routers/_app.test.ts +60 -0
- package/bundles/nextjs-trpc/server/routers/_app.ts +25 -0
- package/bundles/nextjs-trpc/server/trpc.ts +12 -0
- package/bundles/nextjs-trpc/tsconfig.json +42 -0
- package/bundles/nextjs-trpc/vendor/m2ai-braces-3.0.3-m2ai.1.tar +0 -0
- package/dist/agent-install-session.d.ts +17 -0
- package/dist/agent-install-session.d.ts.map +1 -0
- package/dist/agent-install-session.js +106 -0
- package/dist/agent-install-session.js.map +1 -0
- package/dist/agent-install-session.test.d.ts +2 -0
- package/dist/agent-install-session.test.d.ts.map +1 -0
- package/dist/agent-install-session.test.js +62 -0
- package/dist/agent-install-session.test.js.map +1 -0
- package/dist/agent-install.d.ts +41 -0
- package/dist/agent-install.d.ts.map +1 -0
- package/dist/agent-install.js +42 -0
- package/dist/agent-install.js.map +1 -0
- package/dist/agent-install.test.d.ts +2 -0
- package/dist/agent-install.test.d.ts.map +1 -0
- package/dist/agent-install.test.js +58 -0
- package/dist/agent-install.test.js.map +1 -0
- package/dist/apply-merge.test.d.ts +2 -0
- package/dist/apply-merge.test.d.ts.map +1 -0
- package/dist/apply-merge.test.js +123 -0
- package/dist/apply-merge.test.js.map +1 -0
- package/dist/bundle-download.d.ts +23 -0
- package/dist/bundle-download.d.ts.map +1 -0
- package/dist/bundle-download.js +180 -0
- package/dist/bundle-download.js.map +1 -0
- package/dist/bundle-download.test.d.ts +2 -0
- package/dist/bundle-download.test.d.ts.map +1 -0
- package/dist/bundle-download.test.js +200 -0
- package/dist/bundle-download.test.js.map +1 -0
- package/dist/bundle-rollback.test.d.ts +2 -0
- package/dist/bundle-rollback.test.d.ts.map +1 -0
- package/dist/bundle-rollback.test.js +127 -0
- package/dist/bundle-rollback.test.js.map +1 -0
- package/dist/bundle-scaffold.test.d.ts +2 -0
- package/dist/bundle-scaffold.test.d.ts.map +1 -0
- package/dist/bundle-scaffold.test.js +105 -0
- package/dist/bundle-scaffold.test.js.map +1 -0
- package/dist/commands.d.ts +132 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +2012 -0
- package/dist/commands.js.map +1 -0
- package/dist/credential-store.d.ts +27 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +48 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/credential-store.test.d.ts +2 -0
- package/dist/credential-store.test.d.ts.map +1 -0
- package/dist/credential-store.test.js +80 -0
- package/dist/credential-store.test.js.map +1 -0
- package/dist/delivered-project-files.d.ts +31 -0
- package/dist/delivered-project-files.d.ts.map +1 -0
- package/dist/delivered-project-files.js +158 -0
- package/dist/delivered-project-files.js.map +1 -0
- package/dist/delivered-project-files.test.d.ts +2 -0
- package/dist/delivered-project-files.test.d.ts.map +1 -0
- package/dist/delivered-project-files.test.js +107 -0
- package/dist/delivered-project-files.test.js.map +1 -0
- package/dist/doctor-agents.test.d.ts +2 -0
- package/dist/doctor-agents.test.d.ts.map +1 -0
- package/dist/doctor-agents.test.js +65 -0
- package/dist/doctor-agents.test.js.map +1 -0
- package/dist/hermes-install.d.ts +102 -0
- package/dist/hermes-install.d.ts.map +1 -0
- package/dist/hermes-install.js +641 -0
- package/dist/hermes-install.js.map +1 -0
- package/dist/hermes-install.test.d.ts +2 -0
- package/dist/hermes-install.test.d.ts.map +1 -0
- package/dist/hermes-install.test.js +866 -0
- package/dist/hermes-install.test.js.map +1 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5311 -0
- package/dist/index.js.map +1 -0
- package/dist/openclaw-install.d.ts +67 -0
- package/dist/openclaw-install.d.ts.map +1 -0
- package/dist/openclaw-install.js +358 -0
- package/dist/openclaw-install.js.map +1 -0
- package/dist/openclaw-install.test.d.ts +2 -0
- package/dist/openclaw-install.test.d.ts.map +1 -0
- package/dist/openclaw-install.test.js +296 -0
- package/dist/openclaw-install.test.js.map +1 -0
- package/dist/password-recovery-bundle.test.d.ts +2 -0
- package/dist/password-recovery-bundle.test.d.ts.map +1 -0
- package/dist/password-recovery-bundle.test.js +132 -0
- package/dist/password-recovery-bundle.test.js.map +1 -0
- package/dist/project-bundle-env.d.ts +19 -0
- package/dist/project-bundle-env.d.ts.map +1 -0
- package/dist/project-bundle-env.js +111 -0
- package/dist/project-bundle-env.js.map +1 -0
- package/dist/project-bundle-env.test.d.ts +2 -0
- package/dist/project-bundle-env.test.d.ts.map +1 -0
- package/dist/project-bundle-env.test.js +93 -0
- package/dist/project-bundle-env.test.js.map +1 -0
- package/dist/project-path-confinement.test.d.ts +2 -0
- package/dist/project-path-confinement.test.d.ts.map +1 -0
- package/dist/project-path-confinement.test.js +222 -0
- package/dist/project-path-confinement.test.js.map +1 -0
- package/dist/security/api.d.ts +18 -0
- package/dist/security/api.d.ts.map +1 -0
- package/dist/security/api.js +238 -0
- package/dist/security/api.js.map +1 -0
- package/dist/security/api.test.d.ts +2 -0
- package/dist/security/api.test.d.ts.map +1 -0
- package/dist/security/api.test.js +220 -0
- package/dist/security/api.test.js.map +1 -0
- package/dist/security/artifact.d.ts +10 -0
- package/dist/security/artifact.d.ts.map +1 -0
- package/dist/security/artifact.js +283 -0
- package/dist/security/artifact.js.map +1 -0
- package/dist/security/artifact.test.d.ts +2 -0
- package/dist/security/artifact.test.d.ts.map +1 -0
- package/dist/security/artifact.test.js +229 -0
- package/dist/security/artifact.test.js.map +1 -0
- package/dist/security/commands.d.ts +42 -0
- package/dist/security/commands.d.ts.map +1 -0
- package/dist/security/commands.js +825 -0
- package/dist/security/commands.js.map +1 -0
- package/dist/security/commands.test.d.ts +2 -0
- package/dist/security/commands.test.d.ts.map +1 -0
- package/dist/security/commands.test.js +1051 -0
- package/dist/security/commands.test.js.map +1 -0
- package/dist/security/connection.d.ts +19 -0
- package/dist/security/connection.d.ts.map +1 -0
- package/dist/security/connection.js +145 -0
- package/dist/security/connection.js.map +1 -0
- package/dist/security/connection.test.d.ts +2 -0
- package/dist/security/connection.test.d.ts.map +1 -0
- package/dist/security/connection.test.js +62 -0
- package/dist/security/connection.test.js.map +1 -0
- package/dist/security/index-routing.test.d.ts +2 -0
- package/dist/security/index-routing.test.d.ts.map +1 -0
- package/dist/security/index-routing.test.js +169 -0
- package/dist/security/index-routing.test.js.map +1 -0
- package/dist/security/journal.d.ts +26 -0
- package/dist/security/journal.d.ts.map +1 -0
- package/dist/security/journal.js +441 -0
- package/dist/security/journal.js.map +1 -0
- package/dist/security/journal.test.d.ts +2 -0
- package/dist/security/journal.test.d.ts.map +1 -0
- package/dist/security/journal.test.js +226 -0
- package/dist/security/journal.test.js.map +1 -0
- package/dist/security/package-verifier.d.ts +15 -0
- package/dist/security/package-verifier.d.ts.map +1 -0
- package/dist/security/package-verifier.js +65 -0
- package/dist/security/package-verifier.js.map +1 -0
- package/dist/security/package-verifier.test.d.ts +2 -0
- package/dist/security/package-verifier.test.d.ts.map +1 -0
- package/dist/security/package-verifier.test.js +57 -0
- package/dist/security/package-verifier.test.js.map +1 -0
- package/dist/security/runtime.d.ts +43 -0
- package/dist/security/runtime.d.ts.map +1 -0
- package/dist/security/runtime.js +81 -0
- package/dist/security/runtime.js.map +1 -0
- package/dist/security/runtime.test.d.ts +2 -0
- package/dist/security/runtime.test.d.ts.map +1 -0
- package/dist/security/runtime.test.js +88 -0
- package/dist/security/runtime.test.js.map +1 -0
- package/dist/security/types.d.ts +169 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +3 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security-runtime/LICENSE +202 -0
- package/dist/security-runtime/NOTICE +16 -0
- package/dist/security-runtime/artifact-manifest.json +960 -0
- package/dist/security-runtime/bin/vsg.mjs +5 -0
- package/dist/security-runtime/lattice-catalog.json +176585 -0
- package/dist/security-runtime/lattice-runtime.mjs +90 -0
- package/dist/security-runtime/licensing/candidates/apache-source-license-evidence.json +478 -0
- package/dist/security-runtime/licensing/candidates/betterleaks-generic-api-key.json +76 -0
- package/dist/security-runtime/licensing/candidates/betterleaks-jwt.json +74 -0
- package/dist/security-runtime/licensing/candidates/blackberry-additional-source-declaration-evidence.json +276 -0
- package/dist/security-runtime/licensing/candidates/blackberry-source-declaration-evidence.json +369 -0
- package/dist/security-runtime/licensing/candidates/cado-source-declaration-evidence.json +283 -0
- package/dist/security-runtime/licensing/candidates/commercial-rule-candidate.json +280 -0
- package/dist/security-runtime/licensing/candidates/commercial-runtime-release.json +389 -0
- package/dist/security-runtime/licensing/candidates/data-feed-evidence.json +259 -0
- package/dist/security-runtime/licensing/candidates/gitlab-sast-rule-identities.json +178 -0
- package/dist/security-runtime/licensing/candidates/gitlab-sast-source-evidence.json +1824 -0
- package/dist/security-runtime/licensing/candidates/gitlab-sast-source-notice-bundle.json +45 -0
- package/dist/security-runtime/licensing/candidates/malpedia-source-license-evidence.json +41 -0
- package/dist/security-runtime/licensing/candidates/rule-license-evidence.json +376 -0
- package/dist/security-runtime/licensing/candidates/runner-image-composition.json +1500 -0
- package/dist/security-runtime/licensing/candidates/selected-runtime-coverage.json +91 -0
- package/dist/security-runtime/licensing/candidates/yara-forge-20260927.json +58502 -0
- package/dist/security-runtime/licensing/commercial-components.json +379 -0
- package/dist/security-runtime/licensing/notices/betterleaks-1.9.0-MIT.txt +21 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5.txt +428 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/33acf42afdd03163d75fe856cb431e70710e0b5deede3362e6d1c4697cfe1c6a.txt +28 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986.txt +674 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/4fecb8fcaf80fd9a6315bca5225ee43377015b46a0459c2925495be885cab19a.txt +21 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/811515f8723aa4007de2fbb75a364127fdec51cda534b87a7ea8f2fef2f96dd7.txt +24 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/814442c3987a2c0411171ad8ad43118e8c08f85c9c5d1dcdecc9787c7ca8689b.txt +664 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/8a959b2413d0c83ca1b3ce9153076c649abf2e4bbfebbb493fb24c2074a1eb8a.txt +38 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/995182e185856186fbbe49f150409ae026aa4cda7e94f83c6ed737cbb77fa9c7.txt +23 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/9b8e1305e46d1dd4011b6ed933c9f6223e08c9c4625001c5fd75a34fa930f616.txt +202 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/a01f1a28424bfe7f49a23d339a01847cfa9ace3864d5e7295051772a4bae2271.txt +38 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/a5681bf9b05db14d86776930017c647ad9e6e56ff6bbcfdf21e5848288dfaf1b.txt +165 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/a5b8bd9ae96c630bbf229d61c5c5def20274e00be39abfd2fab4d7879ceab462.txt +24 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/ada32b3747ce108d5e8bab7918af94abbd03f2fe2a46304f74c6333bbfd914ed.txt +8 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4.txt +201 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/c738ae413cf561f174e34f6961f8ca458aae2369a73640dda6234c629b98bcc4.txt +2732 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/ce41c9a2c986277f7f9ab146a2195c1888a5d8da062e22d7c9c88666bb7c38cd.txt +17 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/d5f4c36a0528e01cfdd9e4fc60879201d3af49b41c520200643d321fe53da5f9.txt +17 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/e08c368ccd455f85c7fb9e04830e730f26e9dbb5c0f77bc88e09a5b0cb37df5b.txt +38 -0
- package/dist/security-runtime/licensing/notices/candidate-sources/sha256/e9efb2f7daf11bb6379c06c16b870dddcecc4a10f8704bb8aad807529c8b3c97.txt +8 -0
- package/dist/security-runtime/licensing/notices/gitleaks-generic-origin-782f3104-MIT.txt +21 -0
- package/dist/security-runtime/package.json +17 -0
- package/dist/security-runtime/playbook/README.md +76 -0
- package/dist/security-runtime/playbook/access-control.md +56 -0
- package/dist/security-runtime/playbook/ai-features.md +53 -0
- package/dist/security-runtime/playbook/desktop-python.md +59 -0
- package/dist/security-runtime/playbook/input-handling.md +59 -0
- package/dist/security-runtime/playbook/mobile-expo.md +59 -0
- package/dist/security-runtime/playbook/payments.md +53 -0
- package/dist/security-runtime/playbook/privacy-account-recovery.md +59 -0
- package/dist/security-runtime/playbook/secrets-config.md +47 -0
- package/dist/security-runtime/playbook/supabase.md +48 -0
- package/dist/security-runtime/playbook/supply-chain.md +62 -0
- package/dist/security-runtime/playbook/webhooks-integrations.md +52 -0
- package/dist/security-runtime/rules/vsg-ai-key-in-client.yaml +41 -0
- package/dist/security-runtime/rules/vsg-ai-output-raw-html.yaml +35 -0
- package/dist/security-runtime/rules/vsg-angular-sanitizer-bypass.yaml +23 -0
- package/dist/security-runtime/rules/vsg-expo-token-in-asyncstorage.yaml +22 -0
- package/dist/security-runtime/rules/vsg-flask-public-bind.yaml +32 -0
- package/dist/security-runtime/rules/vsg-nextjs-action-write-no-auth.yaml +144 -0
- package/dist/security-runtime/rules/vsg-nextjs-inline-action-write-no-auth.yaml +161 -0
- package/dist/security-runtime/rules/vsg-nextjs-route-write-no-auth.yaml +193 -0
- package/dist/security-runtime/rules/vsg-nosql-where-injection.yaml +36 -0
- package/dist/security-runtime/rules/vsg-python-pickle-load.yaml +26 -0
- package/dist/security-runtime/rules/vsg-python-requests-verify-false.yaml +34 -0
- package/dist/security-runtime/rules/vsg-python-subprocess-shell.yaml +23 -0
- package/dist/security-runtime/rules/vsg-stripe-client-price.yaml +51 -0
- package/dist/security-runtime/rules/vsg-stripe-webhook-no-signature.yaml +31 -0
- package/dist/security-runtime/rules/vsg-stripe-webhook-pages-no-signature.yaml +51 -0
- package/dist/security-runtime/rules/vsg-supabase-service-role-in-client.yaml +27 -0
- package/dist/security-runtime/rules/vsg-supabase-service-role-public-env.yaml +24 -0
- package/dist/security-runtime/rules/vsg-xml-external-entities.yaml +19 -0
- package/dist/security-runtime/src/agent/brief.mjs +95 -0
- package/dist/security-runtime/src/agent/discover.mjs +108 -0
- package/dist/security-runtime/src/agent/exec.mjs +64 -0
- package/dist/security-runtime/src/agent/ingest.mjs +213 -0
- package/dist/security-runtime/src/agent/report.mjs +94 -0
- package/dist/security-runtime/src/agent/schema.mjs +62 -0
- package/dist/security-runtime/src/agent/tokenize.mjs +38 -0
- package/dist/security-runtime/src/checks/README.md +8 -0
- package/dist/security-runtime/src/checks/_lib/gitfiles.mjs +35 -0
- package/dist/security-runtime/src/checks/_lib/lockfiles.mjs +135 -0
- package/dist/security-runtime/src/checks/_lib/osv.mjs +6 -0
- package/dist/security-runtime/src/checks/_lib/secrets.mjs +381 -0
- package/dist/security-runtime/src/checks/_lib/targets.mjs +79 -0
- package/dist/security-runtime/src/checks/agent-review/index.mjs +52 -0
- package/dist/security-runtime/src/checks/agent-surface/config.mjs +223 -0
- package/dist/security-runtime/src/checks/agent-surface/index.mjs +113 -0
- package/dist/security-runtime/src/checks/agent-surface/instructions.mjs +87 -0
- package/dist/security-runtime/src/checks/agent-surface/patterns.mjs +68 -0
- package/dist/security-runtime/src/checks/agent-surface/unicode.mjs +88 -0
- package/dist/security-runtime/src/checks/code/auth-helpers.mjs +659 -0
- package/dist/security-runtime/src/checks/code/index.mjs +617 -0
- package/dist/security-runtime/src/checks/code/opengrep-container.mjs +85 -0
- package/dist/security-runtime/src/checks/code/opengrep-runner.mjs +86 -0
- package/dist/security-runtime/src/checks/code/selected-opengrep-runner.mjs +112 -0
- package/dist/security-runtime/src/checks/dep-change/guarddog.mjs +142 -0
- package/dist/security-runtime/src/checks/dep-change/index.mjs +316 -0
- package/dist/security-runtime/src/checks/dep-change/registries.mjs +199 -0
- package/dist/security-runtime/src/checks/deps-vuln/index.mjs +305 -0
- package/dist/security-runtime/src/checks/desktop-config/index.mjs +112 -0
- package/dist/security-runtime/src/checks/dns-mail/index.mjs +71 -0
- package/dist/security-runtime/src/checks/iac/index.mjs +83 -0
- package/dist/security-runtime/src/checks/install-posture/index.mjs +123 -0
- package/dist/security-runtime/src/checks/licenses/index.mjs +143 -0
- package/dist/security-runtime/src/checks/live-basic/index.mjs +77 -0
- package/dist/security-runtime/src/checks/live-js-secrets/index.mjs +100 -0
- package/dist/security-runtime/src/checks/malware/clamav.mjs +17 -0
- package/dist/security-runtime/src/checks/malware/defender.mjs +43 -0
- package/dist/security-runtime/src/checks/malware/index.mjs +186 -0
- package/dist/security-runtime/src/checks/malware/targets.mjs +147 -0
- package/dist/security-runtime/src/checks/malware/yara-runner.mjs +201 -0
- package/dist/security-runtime/src/checks/malware/yara.mjs +124 -0
- package/dist/security-runtime/src/checks/mobile-config/index.mjs +173 -0
- package/dist/security-runtime/src/checks/mobile-config/mobsf.mjs +105 -0
- package/dist/security-runtime/src/checks/posture/index.mjs +133 -0
- package/dist/security-runtime/src/checks/project-scripts/index.mjs +187 -0
- package/dist/security-runtime/src/checks/sbom/index.mjs +98 -0
- package/dist/security-runtime/src/checks/secrets-build/index.mjs +63 -0
- package/dist/security-runtime/src/checks/secrets-history/index.mjs +112 -0
- package/dist/security-runtime/src/checks/secrets-tree/env.mjs +120 -0
- package/dist/security-runtime/src/checks/secrets-tree/index.mjs +73 -0
- package/dist/security-runtime/src/checks/supabase-static/index.mjs +240 -0
- package/dist/security-runtime/src/checks/supabase-static/sql.mjs +557 -0
- package/dist/security-runtime/src/checks/workflows/index.mjs +93 -0
- package/dist/security-runtime/src/cli/deep.mjs +13 -0
- package/dist/security-runtime/src/cli/doctor.mjs +82 -0
- package/dist/security-runtime/src/cli/index.mjs +60 -0
- package/dist/security-runtime/src/cli/live-common.mjs +27 -0
- package/dist/security-runtime/src/cli/parse.mjs +56 -0
- package/dist/security-runtime/src/cli/portfolio.mjs +34 -0
- package/dist/security-runtime/src/cli/release.mjs +28 -0
- package/dist/security-runtime/src/cli/report.mjs +18 -0
- package/dist/security-runtime/src/cli/run-common.mjs +54 -0
- package/dist/security-runtime/src/cli/scan.mjs +12 -0
- package/dist/security-runtime/src/cli/setup.mjs +59 -0
- package/dist/security-runtime/src/cli/url.mjs +38 -0
- package/dist/security-runtime/src/core/accept.mjs +141 -0
- package/dist/security-runtime/src/core/archive.mjs +114 -0
- package/dist/security-runtime/src/core/base-ref.mjs +43 -0
- package/dist/security-runtime/src/core/checks.mjs +39 -0
- package/dist/security-runtime/src/core/detect.mjs +90 -0
- package/dist/security-runtime/src/core/errors.mjs +5 -0
- package/dist/security-runtime/src/core/fetch.mjs +139 -0
- package/dist/security-runtime/src/core/findings.mjs +177 -0
- package/dist/security-runtime/src/core/gate.mjs +131 -0
- package/dist/security-runtime/src/core/installer.mjs +255 -0
- package/dist/security-runtime/src/core/lock.mjs +38 -0
- package/dist/security-runtime/src/core/nested.mjs +107 -0
- package/dist/security-runtime/src/core/paths.mjs +20 -0
- package/dist/security-runtime/src/core/pipeline.mjs +196 -0
- package/dist/security-runtime/src/core/registry.mjs +81 -0
- package/dist/security-runtime/src/core/report.mjs +128 -0
- package/dist/security-runtime/src/core/runner.mjs +86 -0
- package/dist/security-runtime/src/core/sarif.mjs +141 -0
- package/dist/security-runtime/src/lattice/artifact.mjs +1201 -0
- package/dist/security-runtime/src/lattice/execution.mjs +45 -0
- package/dist/security-runtime/src/lattice/export.mjs +215 -0
- package/dist/security-runtime/src/lattice/provenance.mjs +70 -0
- package/dist/security-runtime/src/lattice/runtime.mjs +158 -0
- package/dist/security-runtime/src/lattice/selected-runtime.mjs +189 -0
- package/dist/security-runtime/src/lattice/source-acquirer.mjs +51 -0
- package/dist/security-runtime/src/lattice/source-fetcher.mjs +88 -0
- package/dist/security-runtime/src/lattice/transport.mjs +232 -0
- package/dist/security-runtime/src/live/analyze.mjs +101 -0
- package/dist/security-runtime/src/live/exposure.mjs +79 -0
- package/dist/security-runtime/src/live/secrets.mjs +31 -0
- package/dist/security-runtime/src/live/session.mjs +98 -0
- package/dist/security-runtime/src/live/targets.mjs +81 -0
- package/dist/security-runtime/src/portfolio/index.mjs +116 -0
- package/dist/security-runtime/tools/registry/betterleaks.json +29 -0
- package/dist/security-runtime/tools/registry/guarddog.json +9 -0
- package/dist/security-runtime/tools/registry/mobsf.json +10 -0
- package/dist/security-runtime/tools/registry/opengrep.json +28 -0
- package/dist/security-runtime/tools/registry/osv-scanner.json +14 -0
- package/dist/security-runtime/tools/registry/semgrep-rules.json +21 -0
- package/dist/security-runtime/tools/registry/trivy.json +30 -0
- package/dist/security-runtime/tools/registry/yara-forge-exclusions.json +14 -0
- package/dist/security-runtime/tools/registry/yara-forge.json +22 -0
- package/dist/security-runtime/tools/registry/yara-runner.json +10 -0
- package/dist/security-runtime/tools/registry/yara-x.json +24 -0
- package/dist/security-runtime/tools/registry/zizmor.json +30 -0
- package/dist/spinner.d.ts +16 -0
- package/dist/spinner.d.ts.map +1 -0
- package/dist/spinner.js +72 -0
- package/dist/spinner.js.map +1 -0
- package/dist/verify.test.d.ts +2 -0
- package/dist/verify.test.d.ts.map +1 -0
- package/dist/verify.test.js +98 -0
- package/dist/verify.test.js.map +1 -0
- package/package.json +73 -4
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// project-scripts (REQ-019): runs the project's own typecheck, lint, test and build scripts (and pytest) with CI=1 and records
|
|
2
|
+
// result, duration and the output tail. This check EXECUTES PROJECT CODE, so it runs only in deep and release (never scan), only
|
|
3
|
+
// when dependencies are already installed (it never installs anything), and with the runner's environment allowlist (no tokens or cloud keys).
|
|
4
|
+
// Release mode: a failing typecheck, test or build blocks (severity high, category "release"); a failing lint is only a warning.
|
|
5
|
+
// Deep mode: every failure is a warning (medium).
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { scrubText } from '../../core/findings.mjs';
|
|
9
|
+
|
|
10
|
+
export const id = 'project-scripts';
|
|
11
|
+
export const title = 'Project typecheck, lint, test and build scripts';
|
|
12
|
+
export const modes = ['deep', 'release'];
|
|
13
|
+
export const requiredIn = ['release'];
|
|
14
|
+
// Project scripts may create build output. Run this phase before the artifact scanners.
|
|
15
|
+
export const phase = 1;
|
|
16
|
+
export const SCRIPT_TIMEOUT_SEC = 20 * 60;
|
|
17
|
+
export const timeoutSec = 5 * SCRIPT_TIMEOUT_SEC + 300; // four scripts plus pytest, each with its own 20 minute limit
|
|
18
|
+
export const TAIL_LINES = 40;
|
|
19
|
+
|
|
20
|
+
// Script names are fixed constants chosen here, never taken from package.json, so every token passed to the package manager is ours.
|
|
21
|
+
const SCRIPTS = [
|
|
22
|
+
{ name: 'typecheck', candidates: ['typecheck', 'type-check', 'tsc'], blocksInRelease: true },
|
|
23
|
+
{ name: 'lint', candidates: ['lint'], blocksInRelease: false },
|
|
24
|
+
{ name: 'test', candidates: ['test'], blocksInRelease: true },
|
|
25
|
+
{ name: 'build', candidates: ['build'], blocksInRelease: true },
|
|
26
|
+
];
|
|
27
|
+
const MANAGER_ORDER = ['pnpm', 'yarn', 'bun', 'npm']; // lockfile-derived manager names from detect.mjs
|
|
28
|
+
const NOTICE = 'vsg executed the project\'s own scripts (project code ran) with CI=1 and an allowlisted environment: no tokens or cloud keys were passed, and nothing was installed.';
|
|
29
|
+
|
|
30
|
+
const exists = (p) => { try { return fs.statSync(p); } catch { return null; } };
|
|
31
|
+
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
|
|
32
|
+
const read = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } };
|
|
33
|
+
|
|
34
|
+
function hasPytest(repo) {
|
|
35
|
+
if (exists(path.join(repo, 'pytest.ini')) || exists(path.join(repo, 'conftest.py'))) return true;
|
|
36
|
+
if (exists(path.join(repo, 'tests'))?.isDirectory()) return true;
|
|
37
|
+
return /\[pytest\]/.test(read(path.join(repo, 'tox.ini'))) || /\[tool:pytest\]/.test(read(path.join(repo, 'setup.cfg')))
|
|
38
|
+
|| /\[tool\.pytest/.test(read(path.join(repo, 'pyproject.toml')));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function applies(ctx) {
|
|
42
|
+
return exists(path.join(ctx.repo, 'package.json')) || hasPytest(ctx.repo)
|
|
43
|
+
? true
|
|
44
|
+
: { applies: false, reason: 'no package.json and no pytest configuration or tests directory' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Finds an executable on PATH (the current directory is never searched). Returns the full path or null. */
|
|
48
|
+
export function findOnPath(name, env = process.env) {
|
|
49
|
+
const win = process.platform === 'win32';
|
|
50
|
+
const exts = win ? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean) : [''];
|
|
51
|
+
const dirs = (env.PATH || env.Path || '').split(path.delimiter).filter(Boolean);
|
|
52
|
+
for (const dir of dirs) {
|
|
53
|
+
for (const ext of exts) {
|
|
54
|
+
const full = path.join(dir, `${name}${ext}`);
|
|
55
|
+
if (exists(full)?.isFile()) return full;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Builds the spawn arguments for "<manager> run <script>" without a shell string.
|
|
63
|
+
* Node 22 refuses to spawn .cmd files directly on Windows, and the package managers are .cmd shims there. The shim is started through
|
|
64
|
+
* cmd.exe with every argument passed separately (cmd.exe /d /s /c <manager> run <script>): all tokens are fixed constants made of
|
|
65
|
+
* letters and dashes, so nothing needs quoting and nothing from the repository reaches the command line. Elsewhere (and for bun.exe)
|
|
66
|
+
* the resolved executable is spawned directly.
|
|
67
|
+
*/
|
|
68
|
+
export function packageManagerCommand(manager, script, resolved) {
|
|
69
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
|
|
70
|
+
return { command: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', manager, 'run', script] };
|
|
71
|
+
}
|
|
72
|
+
return { command: resolved, args: ['run', script] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, 'g');
|
|
76
|
+
export function tailOf(stdout, stderr, lines = TAIL_LINES) {
|
|
77
|
+
const all = `${stdout}\n${stderr}`.replace(ANSI, '').split(/\r?\n/).map((l) => l.trimEnd());
|
|
78
|
+
while (all.length && !all[all.length - 1]) all.pop();
|
|
79
|
+
return scrubText(all.slice(-lines).join('\n'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function chooseManager(ctx) {
|
|
83
|
+
const roots = (ctx.detection.lockfiles ?? []).filter((l) => !l.file.includes('/')).map((l) => l.manager);
|
|
84
|
+
const found = MANAGER_ORDER.find((m) => roots.includes(m));
|
|
85
|
+
return { manager: found ?? 'npm', fromLockfile: !!found };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function execScript(ctx, { name, label, command, args, extraEnvNames = [] }) {
|
|
89
|
+
const r = await ctx.runProcess({
|
|
90
|
+
command, args, cwd: ctx.repo, timeoutMs: SCRIPT_TIMEOUT_SEC * 1000, maxOutputBytes: 8 * 1024 * 1024,
|
|
91
|
+
envNames: ['PNPM_HOME', ...extraEnvNames], extraEnv: { CI: '1' },
|
|
92
|
+
});
|
|
93
|
+
const base = { name, command: label, durationMs: r.durationMs };
|
|
94
|
+
if (r.status !== 'ran') return { ...base, status: /timed out/.test(r.reason ?? '') ? 'timed_out' : 'not_run', reason: r.reason, outputTail: tailOf(r.stdout ?? '', r.stderr ?? '') };
|
|
95
|
+
return { ...base, status: r.exitCode === 0 ? 'passed' : 'failed', exitCode: r.exitCode, outputTail: tailOf(r.stdout, r.stderr) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function run(ctx) {
|
|
99
|
+
const results = {};
|
|
100
|
+
const evidenceNote = { notice: NOTICE };
|
|
101
|
+
const release = ctx.mode === 'release';
|
|
102
|
+
const notRunReasons = [];
|
|
103
|
+
|
|
104
|
+
// ---- package.json scripts ----
|
|
105
|
+
const pkgPath = path.join(ctx.repo, 'package.json');
|
|
106
|
+
if (exists(pkgPath)) {
|
|
107
|
+
const scripts = readJson(pkgPath)?.scripts ?? {};
|
|
108
|
+
const planned = SCRIPTS.map((s) => {
|
|
109
|
+
const script = s.candidates.find((c) => typeof scripts[c] === 'string');
|
|
110
|
+
if (!script) return { ...s, status: 'not_applicable', reason: `package.json has no ${s.candidates.join('/')} script` };
|
|
111
|
+
if (s.name === 'test' && /no test specified/i.test(scripts[script])) return { ...s, status: 'not_applicable', reason: 'the test script is the npm init placeholder' };
|
|
112
|
+
return { ...s, script };
|
|
113
|
+
});
|
|
114
|
+
const { manager, fromLockfile } = chooseManager(ctx);
|
|
115
|
+
evidenceNote.packageManager = `${manager}${fromLockfile ? ' (from the lockfile)' : ' (no lockfile found: default)'}`;
|
|
116
|
+
const toRun = planned.filter((p) => p.script);
|
|
117
|
+
for (const p of planned.filter((x) => !x.script)) results[p.name] = { name: p.name, status: 'not_applicable', reason: p.reason };
|
|
118
|
+
if (toRun.length) {
|
|
119
|
+
const modules = exists(path.join(ctx.repo, 'node_modules'));
|
|
120
|
+
const resolved = findOnPath(manager);
|
|
121
|
+
let why = '';
|
|
122
|
+
if (!modules?.isDirectory()) why = 'dependencies not installed';
|
|
123
|
+
else if (!resolved) why = `${manager} was not found on PATH`;
|
|
124
|
+
if (why) {
|
|
125
|
+
for (const p of toRun) results[p.name] = { name: p.name, script: p.script, status: 'not_run', reason: why };
|
|
126
|
+
notRunReasons.push(`${toRun.map((p) => p.name).join(', ')}: ${why}`);
|
|
127
|
+
} else {
|
|
128
|
+
for (const p of toRun) {
|
|
129
|
+
const { command, args } = packageManagerCommand(manager, p.script, resolved);
|
|
130
|
+
results[p.name] = { ...(await execScript(ctx, { name: p.name, label: `${manager} run ${p.script}`, command, args })), script: p.script, blocksInRelease: p.blocksInRelease };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- pytest ----
|
|
137
|
+
if (hasPytest(ctx.repo)) {
|
|
138
|
+
const python = ['python', 'python3'].map((n) => findOnPath(n)).find(Boolean);
|
|
139
|
+
if (!python) {
|
|
140
|
+
results.pytest = { name: 'pytest', status: 'not_run', reason: 'python was not found on PATH' };
|
|
141
|
+
notRunReasons.push('pytest: python was not found on PATH');
|
|
142
|
+
} else {
|
|
143
|
+
const r = await execScript(ctx, { name: 'pytest', label: 'python -m pytest -q', command: python, args: ['-m', 'pytest', '-q'], extraEnvNames: ['VIRTUAL_ENV', 'PYTHONPATH'] });
|
|
144
|
+
if (r.status === 'failed' && /No module named '?pytest/.test(r.outputTail)) {
|
|
145
|
+
results.pytest = { name: 'pytest', status: 'not_run', reason: 'pytest is not installed in this Python environment' };
|
|
146
|
+
notRunReasons.push('pytest: pytest is not installed in this Python environment');
|
|
147
|
+
} else if (r.status === 'failed' && r.exitCode === 5) {
|
|
148
|
+
results.pytest = { ...r, status: 'not_applicable', reason: 'pytest collected no tests' };
|
|
149
|
+
} else results.pytest = { ...r, blocksInRelease: true };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- findings ----
|
|
154
|
+
const findings = [];
|
|
155
|
+
for (const r of Object.values(results)) {
|
|
156
|
+
if (r.status !== 'failed' && r.status !== 'timed_out') continue;
|
|
157
|
+
const blocks = release && r.blocksInRelease;
|
|
158
|
+
const what = r.status === 'timed_out' ? `timed out after ${SCRIPT_TIMEOUT_SEC / 60} minutes` : `failed with exit code ${r.exitCode}`;
|
|
159
|
+
findings.push({
|
|
160
|
+
tool: 'project-scripts', ruleId: `script-${r.name}`, key: `script-${r.name}`, category: 'release',
|
|
161
|
+
severity: blocks ? 'high' : 'medium', confidence: 'confirmed',
|
|
162
|
+
title: `Project ${r.name} ${r.status === 'timed_out' ? 'timed out' : 'failed'}`,
|
|
163
|
+
description: `\`${r.command}\` ${what} (${Math.round(r.durationMs / 100) / 10} s).${blocks ? '' : release ? ' A failing lint only warns.' : ' Deep mode reports script failures as warnings; release mode blocks on a failing typecheck, test or build.'}`,
|
|
164
|
+
evidence: r.outputTail,
|
|
165
|
+
fix: `Fix the ${r.name} failure and rerun vsg. The last ${TAIL_LINES} lines of output are in raw/project-scripts/${r.name}.txt.`,
|
|
166
|
+
location: { file: r.name === 'pytest' ? 'pytest' : 'package.json' },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const evidence = { 'summary.json': { ...evidenceNote, scripts: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { ...v, outputTail: undefined }])) } };
|
|
171
|
+
for (const r of Object.values(results)) if (r.outputTail) evidence[`${r.name}.txt`] = `$ ${r.command} (exit ${r.exitCode ?? r.status}, ${r.durationMs} ms)\n${r.outputTail}\n`;
|
|
172
|
+
|
|
173
|
+
const ranAny = Object.values(results).some((r) => ['passed', 'failed', 'timed_out'].includes(r.status));
|
|
174
|
+
const executed = ranAny ? NOTICE : 'No project code was executed.';
|
|
175
|
+
// Nothing verified: fail closed. If a release-blocking failure exists the verdict is BLOCKED anyway, so the findings are kept.
|
|
176
|
+
if (notRunReasons.length && !findings.some((f) => f.severity === 'high')) {
|
|
177
|
+
return { status: 'not_run', reason: `${notRunReasons.join('; ')}. ${executed}`, evidence };
|
|
178
|
+
}
|
|
179
|
+
const summary = Object.values(results).map((r) => `${r.name}: ${r.status}`).join(', ');
|
|
180
|
+
return {
|
|
181
|
+
status: 'ran', reason: notRunReasons.length ? `some scripts did not run (${notRunReasons.join('; ')})` : '', findings,
|
|
182
|
+
dataVersions: { projectCode: executed, scripts: summary || 'none' },
|
|
183
|
+
evidence,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default { id, title, modes, requiredIn, phase, timeoutSec, applies, run };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// sbom (REQ-020): a CycloneDX 1.5 SBOM from OSV-Scanner, validated before it is kept. Written to raw/sbom/sbom.cdx.json by the report writer.
|
|
2
|
+
// A SBOM that is malformed, or that is missing a package from a supported lockfile, makes the check not_run so release cannot pass on a partial SBOM.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { osvExcludeArgs } from '../_lib/osv.mjs';
|
|
6
|
+
import { keyOf, lockfileKind, normPyName, parseLockfile } from '../_lib/lockfiles.mjs';
|
|
7
|
+
|
|
8
|
+
export const id = 'sbom';
|
|
9
|
+
export const title = 'CycloneDX 1.5 software bill of materials';
|
|
10
|
+
export const modes = ['release'];
|
|
11
|
+
export const requiredIn = ['release'];
|
|
12
|
+
export const timeoutSec = 600;
|
|
13
|
+
|
|
14
|
+
const base = (f) => f.slice(f.lastIndexOf('/') + 1);
|
|
15
|
+
|
|
16
|
+
export function applies(ctx) {
|
|
17
|
+
const d = ctx.detection;
|
|
18
|
+
return d.lockfiles.length > 0 || (d.manifests ?? []).some((m) => /^requirements.*\.txt$/.test(base(m)))
|
|
19
|
+
? true
|
|
20
|
+
: { applies: false, reason: 'no lockfile or requirements file was found' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Component key in the same form as keyOf: ecosystem:name@version. */
|
|
24
|
+
function componentKey(c) {
|
|
25
|
+
const purl = typeof c.purl === 'string' ? /^pkg:(npm|pypi)\/([^@?#]+)@([^?#]+)/i.exec(c.purl) : null;
|
|
26
|
+
if (purl) {
|
|
27
|
+
const eco = purl[1].toLowerCase() === 'pypi' ? 'PyPI' : 'npm';
|
|
28
|
+
const name = decodeURIComponent(purl[2]);
|
|
29
|
+
return `${eco}:${eco === 'PyPI' ? normPyName(name) : name}@${decodeURIComponent(purl[3])}`;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns a list of problems (empty when the document is a minimal valid CycloneDX 1.5 BOM). */
|
|
35
|
+
export function validateCycloneDx(bom) {
|
|
36
|
+
const problems = [];
|
|
37
|
+
if (!bom || typeof bom !== 'object') return ['not a JSON object'];
|
|
38
|
+
if (bom.bomFormat !== 'CycloneDX') problems.push('bomFormat must be "CycloneDX"');
|
|
39
|
+
if (bom.specVersion !== '1.5') problems.push(`specVersion must be "1.5" (got ${JSON.stringify(bom.specVersion)})`);
|
|
40
|
+
if (!Array.isArray(bom.components)) problems.push('components must be an array');
|
|
41
|
+
else {
|
|
42
|
+
bom.components.forEach((c, i) => {
|
|
43
|
+
if (typeof c?.name !== 'string' || !c.name) problems.push(`components[${i}] has no name`);
|
|
44
|
+
if (!['library', 'application', 'framework', 'container', 'device', 'file', 'firmware', 'operating-system', 'platform', 'machine-learning-model', 'data', 'device-driver', 'cryptographic-asset'].includes(c?.type)) problems.push(`components[${i}] has an invalid type`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return problems.slice(0, 10);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every pinned package of every supported lockfile in the repository, as keyOf keys. */
|
|
51
|
+
export function lockfilePackages(ctx) {
|
|
52
|
+
const files = [...ctx.detection.lockfiles.map((l) => l.file), ...(ctx.detection.manifests ?? [])].filter((f) => lockfileKind(f));
|
|
53
|
+
const keys = new Map();
|
|
54
|
+
const unreadable = [];
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
try {
|
|
57
|
+
for (const p of parseLockfile(file, fs.readFileSync(path.join(ctx.repo, file), 'utf8')) ?? []) keys.set(keyOf(p), `${p.name}@${p.version} (${file})`);
|
|
58
|
+
} catch { unreadable.push(file); }
|
|
59
|
+
}
|
|
60
|
+
const unsupported = ctx.detection.lockfiles.map((l) => l.file).filter((f) => !lockfileKind(f));
|
|
61
|
+
return { keys, unreadable, unsupported };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function run(ctx) {
|
|
65
|
+
const res = await ctx.runTool('osv-scanner', [
|
|
66
|
+
'scan', 'source', '-r', '--format', 'cyclonedx-1-5', '--all-packages', '--no-resolve', '--allow-no-lockfiles', '--no-ignore',
|
|
67
|
+
...osvExcludeArgs(ctx),
|
|
68
|
+
'.',
|
|
69
|
+
]);
|
|
70
|
+
if (res.status !== 'ran') return { status: 'not_run', reason: res.reason };
|
|
71
|
+
if (res.truncated) return { status: 'not_run', reason: 'OSV-Scanner output was too large to read completely.' };
|
|
72
|
+
if (res.exitCode !== 0 && res.exitCode !== 1) {
|
|
73
|
+
const tail = res.stderr.trim().split(/\r?\n/).slice(-3).join(' | ');
|
|
74
|
+
return { status: 'not_run', reason: `OSV-Scanner failed with exit code ${res.exitCode}${tail ? `: ${tail}` : ''}.` };
|
|
75
|
+
}
|
|
76
|
+
let bom;
|
|
77
|
+
try { bom = JSON.parse(res.stdout.replace(/^/, '')); } catch { return { status: 'not_run', reason: 'OSV-Scanner did not return a JSON SBOM.' }; }
|
|
78
|
+
const problems = validateCycloneDx(bom);
|
|
79
|
+
if (problems.length) return { status: 'not_run', reason: `The SBOM is not valid CycloneDX 1.5: ${problems.join('; ')}.` };
|
|
80
|
+
|
|
81
|
+
const { keys, unreadable, unsupported } = lockfilePackages(ctx);
|
|
82
|
+
const have = new Set(bom.components.map(componentKey).filter(Boolean));
|
|
83
|
+
const missing = [...keys].filter(([k]) => !have.has(k)).map(([, label]) => label);
|
|
84
|
+
if (missing.length) {
|
|
85
|
+
return { status: 'not_run', reason: `The SBOM is missing ${missing.length} of ${keys.size} lockfile packages (for example ${missing.slice(0, 3).join(', ')}).` };
|
|
86
|
+
}
|
|
87
|
+
const notes = [
|
|
88
|
+
...(unsupported.length ? [`${unsupported.join(', ')} is not a lockfile type vsg can compare against, so its completeness was not verified`] : []),
|
|
89
|
+
...(unreadable.length ? [`could not read ${unreadable.join(', ')} for the completeness check`] : []),
|
|
90
|
+
];
|
|
91
|
+
return {
|
|
92
|
+
status: 'ran', reason: notes.join('; '), findings: [], toolVersion: ctx.registry?.get('osv-scanner')?.version ?? null,
|
|
93
|
+
dataVersions: { sbom: `CycloneDX 1.5, ${bom.components.length} components, ${keys.size} lockfile packages verified` },
|
|
94
|
+
evidence: { 'sbom.cdx.json': `${JSON.stringify(bom, null, 2)}\n` },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default { id, title, modes, requiredIn, timeoutSec, applies, run };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// secrets-build (REQ-012): Betterleaks over build output (.next, dist, build, out) in deep and release runs.
|
|
2
|
+
// Not a required check: a repository without build output has nothing to scan, and release mode builds first (REQ-019).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { demoJwtFinding, hitToFinding, ROTATE, runBetterleaks, supabasePublicKeyFinding } from '../_lib/secrets.mjs';
|
|
6
|
+
|
|
7
|
+
export const id = 'secrets-build';
|
|
8
|
+
export const title = 'Secrets in build output';
|
|
9
|
+
export const modes = ['deep', 'release'];
|
|
10
|
+
export const requiredIn = [];
|
|
11
|
+
// The check is optional when no build output exists, but becomes required once output is present.
|
|
12
|
+
export const requiredIfApplied = true;
|
|
13
|
+
export const phase = 2;
|
|
14
|
+
export const timeoutSec = 1800;
|
|
15
|
+
|
|
16
|
+
const BUILD_DIRS = new Set(['.next', 'dist', 'build', 'out']);
|
|
17
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.vsg', '.venv', 'venv', '__pycache__', '.pnpm-store']);
|
|
18
|
+
const MAX_DEPTH = 4;
|
|
19
|
+
// framework caches duplicate the real output and are very large
|
|
20
|
+
const SKIP = [String.raw`(?:^|/)node_modules/`, String.raw`(?:^|/)\.next/cache/`, String.raw`^\.vsg/`];
|
|
21
|
+
|
|
22
|
+
/** Build output directories (relative posix paths), found without following symlinks. */
|
|
23
|
+
export function findBuildDirs(repo, nested = []) {
|
|
24
|
+
const found = [];
|
|
25
|
+
const skipNested = new Set(nested.map((n) => n.path));
|
|
26
|
+
const walk = (dir, depth) => {
|
|
27
|
+
let entries;
|
|
28
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
if (!e.isDirectory() || e.isSymbolicLink() || SKIP_DIRS.has(e.name)) continue;
|
|
31
|
+
const full = path.join(dir, e.name);
|
|
32
|
+
if (skipNested.has(path.relative(repo, full).split(path.sep).join('/'))) continue;
|
|
33
|
+
if (BUILD_DIRS.has(e.name)) found.push(path.relative(repo, full).split(path.sep).join('/'));
|
|
34
|
+
else if (depth < MAX_DEPTH) walk(full, depth + 1);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
walk(repo, 0);
|
|
38
|
+
return found.sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function applies(ctx) {
|
|
42
|
+
return findBuildDirs(ctx.repo, ctx.nestedRepos).length > 0 ? true : { applies: false, reason: 'no build output (.next, dist, build or out) found' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function run(ctx) {
|
|
46
|
+
const dirs = findBuildDirs(ctx.repo, ctx.nestedRepos);
|
|
47
|
+
if (!dirs.length) return { status: 'not_applicable', reason: 'no build output (.next, dist, build or out) found' };
|
|
48
|
+
const validate = !ctx.flags?.noValidate;
|
|
49
|
+
const scan = await runBetterleaks(ctx, { kind: 'dir', paths: dirs, validate, skip: SKIP });
|
|
50
|
+
if (scan.status !== 'ran') return { status: 'not_run', reason: scan.reason };
|
|
51
|
+
const demo = demoJwtFinding(scan.demoJwts, { where: 'in build output' });
|
|
52
|
+
const publicKey = supabasePublicKeyFinding(scan.publicKeys, { where: 'in build output' });
|
|
53
|
+
const findings = [...scan.hits.map((h) => hitToFinding(h, {
|
|
54
|
+
category: 'secret', severity: 'critical', where: 'in build output',
|
|
55
|
+
fix: `${ROTATE} Then find which source value or environment variable reached the bundle (public-prefixed variables are compiled into client code) and rebuild.`,
|
|
56
|
+
})), ...(demo ? [demo] : []), ...(publicKey ? [publicKey] : [])];
|
|
57
|
+
return {
|
|
58
|
+
status: 'ran', reason: '', findings, toolVersion: ctx.registry?.get('betterleaks')?.version ?? null,
|
|
59
|
+
evidence: { directories: dirs, hits: scan.hits.length, filteredLocalizationNoise: scan.filtered, suppressed: scan.suppressed, validated: validate },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default { id, title, modes, requiredIn, requiredIfApplied, phase, timeoutSec, applies, run };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// secrets-history (REQ-012): Betterleaks over the full git history (deep and release).
|
|
2
|
+
// Blocking policy: a secret only in history blocks at High unless validated inactive, so inactive ones are emitted at Medium.
|
|
3
|
+
// REQ-037: the same value in several files or commits is one finding; a low-precision generic-rule hit in a file that no longer exists
|
|
4
|
+
// is "history only" and does not block.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { severityRank } from '../../core/findings.mjs';
|
|
8
|
+
import { demoJwtFinding, effectiveSeverity, gitState, hitToFinding, ignoredPaths, isGenericRule, ROTATE, runBetterleaks, skipPatterns, supabasePublicKeyFinding } from '../_lib/secrets.mjs';
|
|
9
|
+
|
|
10
|
+
export const id = 'secrets-history';
|
|
11
|
+
export const title = 'Secrets in git history';
|
|
12
|
+
export const modes = ['deep', 'release'];
|
|
13
|
+
export const requiredIn = ['deep', 'release'];
|
|
14
|
+
export const timeoutSec = 1800;
|
|
15
|
+
|
|
16
|
+
const MAX_LOCATIONS = 25;
|
|
17
|
+
const locationLines = (members) => {
|
|
18
|
+
const lines = members.slice(0, MAX_LOCATIONS).map((m) => {
|
|
19
|
+
const c = m.commits;
|
|
20
|
+
const commits = c.length ? `${c.length} commit${c.length === 1 ? '' : 's'}: ${c.slice(0, 3).map((x) => x.slice(0, 12)).join(', ')}${c.length > 3 ? ', ...' : ''}` : 'no commit';
|
|
21
|
+
return `${m.file}${m.line ? `:${m.line}` : ''} (${m.ruleId}; ${commits})`;
|
|
22
|
+
});
|
|
23
|
+
if (members.length > MAX_LOCATIONS) lines.push(`and ${members.length - MAX_LOCATIONS} more`);
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One group per secret value (hash): the same value in several files or commits is one finding, and its severity is the worst member's.
|
|
29
|
+
* A generic-rule hit whose file no longer exists in the tree is "history only" and capped at Medium; a group is history only when every
|
|
30
|
+
* member is such a hit. Returns [{ rep, members, severity, historyOnly }].
|
|
31
|
+
*/
|
|
32
|
+
export function groupHistoryHits(hits, exists) {
|
|
33
|
+
const base = (h) => (h.validation.state === 'valid' ? 'critical' : h.validation.state === 'inactive' ? 'medium' : 'high');
|
|
34
|
+
const gone = (h) => isGenericRule(h.ruleId) && h.validation.state !== 'valid' && !exists(h.file);
|
|
35
|
+
const sevOf = (h) => effectiveSeverity(h, gone(h) ? 'medium' : base(h));
|
|
36
|
+
const byHash = new Map();
|
|
37
|
+
for (const h of hits) {
|
|
38
|
+
if (!byHash.has(h.hash)) byHash.set(h.hash, []);
|
|
39
|
+
byHash.get(h.hash).push(h);
|
|
40
|
+
}
|
|
41
|
+
return [...byHash.values()].map((members) => {
|
|
42
|
+
members.sort((a, b) => severityRank(sevOf(b)) - severityRank(sevOf(a))
|
|
43
|
+
|| Number(isGenericRule(a.ruleId)) - Number(isGenericRule(b.ruleId))
|
|
44
|
+
|| `${a.file}:${a.line ?? 0}`.localeCompare(`${b.file}:${b.line ?? 0}`));
|
|
45
|
+
const rep = members[0];
|
|
46
|
+
const historyOnly = members.every(gone);
|
|
47
|
+
return { rep, members, severity: sevOf(rep), historyOnly };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function applies(ctx) {
|
|
52
|
+
const s = await gitState(ctx);
|
|
53
|
+
if (!s.usable) return { applies: false, reason: 'not a git repository, so there is no history to scan' };
|
|
54
|
+
if (!s.hasCommits) return { applies: false, reason: 'the repository has no commits yet' };
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function run(ctx) {
|
|
59
|
+
const s = await gitState(ctx);
|
|
60
|
+
// A truncated history cannot support a "no secrets in history" result, so the check does not claim it.
|
|
61
|
+
if (s.shallow) return { status: 'not_run', reason: 'This is a shallow clone, so older commits are missing. Fetch the full history (git fetch --unshallow) and run again.' };
|
|
62
|
+
const validate = !ctx.flags?.noValidate;
|
|
63
|
+
const scan = await runBetterleaks(ctx, { kind: 'git', validate });
|
|
64
|
+
if (scan.status !== 'ran') return { status: 'not_run', reason: scan.reason };
|
|
65
|
+
// A value that is still in the working tree is already reported by secrets-tree (same rule, file and value), so it is not repeated as a
|
|
66
|
+
// history finding. Rotation is required either way and the tree finding says so. The commits are kept in evidence.
|
|
67
|
+
const alsoInTree = [];
|
|
68
|
+
let hits = scan.hits;
|
|
69
|
+
if (hits.length) {
|
|
70
|
+
const tree = await runBetterleaks(ctx, { kind: 'dir', paths: ['.'], validate: false, skip: skipPatterns(await ignoredPaths(ctx)) });
|
|
71
|
+
if (tree.status === 'ran') {
|
|
72
|
+
const keyOf = (h) => JSON.stringify([h.ruleId, h.file, h.hash]);
|
|
73
|
+
const inTree = new Set(tree.hits.map(keyOf));
|
|
74
|
+
hits = hits.filter((h) => {
|
|
75
|
+
if (!inTree.has(keyOf(h))) return true;
|
|
76
|
+
alsoInTree.push({ ruleId: h.ruleId, file: h.file, commits: h.commits.slice(0, 5).map((c) => c.slice(0, 12)), commitCount: h.commits.length });
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const exists = (file) => { try { return fs.existsSync(path.join(ctx.repo, file)); } catch { return false; } };
|
|
82
|
+
const groups = groupHistoryHits(hits, exists);
|
|
83
|
+
const findings = groups.map(({ rep, members, severity, historyOnly }) => {
|
|
84
|
+
const commits = [...new Set(members.flatMap((m) => m.commits))];
|
|
85
|
+
const f = hitToFinding(rep, {
|
|
86
|
+
category: 'secret-history',
|
|
87
|
+
severity,
|
|
88
|
+
where: historyOnly ? 'in git history (history only)' : 'in git history',
|
|
89
|
+
extra: `${commits.length ? ` First seen in commit ${commits[0].slice(0, 12)}${commits.length > 1 ? ` and ${commits.length - 1} more` : ''}.` : ''}${historyOnly ? ' History only: the file no longer exists in the current tree and the rule is a low-precision generic one, so this does not block.' : ''}${members.length > 1 ? ` The same value appears in ${members.length} places (listed in the evidence).` : ''}`,
|
|
90
|
+
fix: `${ROTATE} Removing the file now does not help: the value stays in every earlier commit and clone until the history is rewritten or the credential is revoked. Rotation is the fix; rewrite history only if the repository must stay private to the value.`,
|
|
91
|
+
});
|
|
92
|
+
if (members.length > 1) f.evidence += `\n${members.length} locations:\n${locationLines(members)}`;
|
|
93
|
+
if (rep.validation.state === 'inactive') f.confidence = 'high';
|
|
94
|
+
return f;
|
|
95
|
+
});
|
|
96
|
+
const demo = demoJwtFinding(scan.demoJwts, { where: 'in git history' });
|
|
97
|
+
if (demo) findings.push(demo);
|
|
98
|
+
const publicKey = supabasePublicKeyFinding(scan.publicKeys, { where: 'in git history' });
|
|
99
|
+
if (publicKey) findings.push(publicKey);
|
|
100
|
+
return {
|
|
101
|
+
status: 'ran',
|
|
102
|
+
reason: validate ? '' : 'credential validation was disabled by --no-validate',
|
|
103
|
+
findings, toolVersion: ctx.registry?.get('betterleaks')?.version ?? null,
|
|
104
|
+
evidence: {
|
|
105
|
+
hits: scan.hits.length, reported: findings.length, groupedByValue: hits.length - groups.length, historyOnly: groups.filter((g) => g.historyOnly).length,
|
|
106
|
+
filteredLocalizationNoise: scan.filtered, suppressed: scan.suppressed,
|
|
107
|
+
duplicatesOfWorkingTree: alsoInTree.length, duplicatesOfWorkingTreeSample: alsoInTree.slice(0, 20), validated: validate,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default { id, title, modes, requiredIn, timeoutSec, applies, run };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Environment exposure (REQ-012): secret-named variables with a public (browser-bundled) prefix, and committed .env files.
|
|
2
|
+
// Reports names and locations only. Values are never read into a finding.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { jwtClaims, supabasePublicKeyKind } from '../_lib/secrets.mjs';
|
|
6
|
+
|
|
7
|
+
const PUBLIC_NAME = /\b((?:NEXT_PUBLIC|EXPO_PUBLIC|VITE)_[A-Z0-9_]*[A-Z0-9])\b/g;
|
|
8
|
+
const SECRETISH = /SECRET|SERVICE_ROLE|PRIVATE|PASSWORD|TOKEN/;
|
|
9
|
+
const SAFE = /PUBLISHABLE|ANON|PUBLIC_KEY|SITE_KEY/;
|
|
10
|
+
const TEXT_FILE = /(\.(?:[cm]?[jt]sx?|vue|svelte|astro|html|json|ya?ml|toml|sh)|\.env[^/]*)$/i;
|
|
11
|
+
const SKIP_FILE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|tsconfig[^/]*\.json)$|\.min\.js$/;
|
|
12
|
+
const EXAMPLE = /\.(?:example|sample|template|dist)$/i;
|
|
13
|
+
const MAX_BYTES = 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
const base = (f) => f.slice(f.lastIndexOf('/') + 1);
|
|
16
|
+
export const isEnvFile = (f) => /^\.env(\..+)?$/.test(base(f));
|
|
17
|
+
|
|
18
|
+
/** True when a public-prefixed variable name indicates a secret and is not a known-public key kind. */
|
|
19
|
+
export function isSecretPublicName(name) {
|
|
20
|
+
return /SECRET|SERVICE_ROLE|PRIVATE|PASSWORD/.test(name) || (SECRETISH.test(name) && !SAFE.test(name));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const severityOf = (name) => (/SERVICE_ROLE|SECRET|PRIVATE/.test(name) ? 'critical' : 'high');
|
|
24
|
+
|
|
25
|
+
/** files: relative posix paths; tracked: Set of committed paths or null when there is no git view. Returns raw findings. */
|
|
26
|
+
export function envExposure(repo, files, tracked) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const seen = new Map();
|
|
29
|
+
const byDesign = new Map();
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
if (isEnvFile(file) && !EXAMPLE.test(file)) {
|
|
32
|
+
const committed = tracked ? tracked.has(file) : null;
|
|
33
|
+
out.push({
|
|
34
|
+
category: 'config', severity: committed === false ? 'medium' : 'high', confidence: committed === null ? 'medium' : 'high', key: `env-file:${file}`,
|
|
35
|
+
tool: 'vsg', ruleId: 'committed-env-file', location: { file },
|
|
36
|
+
title: committed === false ? `Environment file ${file} is not ignored by git` : `Environment file ${file} is committed`,
|
|
37
|
+
description: committed === null
|
|
38
|
+
? 'Environment files hold real configuration and secrets and must not be shared with the code. This directory has no git history to confirm whether the file is committed.'
|
|
39
|
+
: committed ? 'Environment files hold real configuration and secrets, and a committed copy stays in history even after it is deleted.'
|
|
40
|
+
: 'The file is not committed yet but .gitignore does not exclude it, so the next "git add ." would commit it.',
|
|
41
|
+
evidence: file, refs: [],
|
|
42
|
+
fix: `Keep only a ${base(file).replace(/^(\.env)(\..+)?$/, '$1')}.example file with placeholder values in git. Add ${file} to .gitignore${committed ? ', run "git rm --cached ' + file + '", and rotate every secret it contained' : ''}.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!TEXT_FILE.test(file) || SKIP_FILE.test(file)) continue;
|
|
46
|
+
let text;
|
|
47
|
+
try {
|
|
48
|
+
const full = path.join(repo, file);
|
|
49
|
+
if (fs.statSync(full).size > MAX_BYTES) continue;
|
|
50
|
+
text = fs.readFileSync(full, 'utf8');
|
|
51
|
+
} catch { continue; }
|
|
52
|
+
const lines = text.split(/\r?\n/);
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
for (const m of lines[i].matchAll(PUBLIC_NAME)) {
|
|
55
|
+
const name = m[1];
|
|
56
|
+
const value = valueAfter(lines[i], m.index + name.length);
|
|
57
|
+
if (!isSecretPublicName(name) && !looksPrivate(value)) continue;
|
|
58
|
+
const k = `${file}\0${name}`;
|
|
59
|
+
if (seen.has(k)) continue;
|
|
60
|
+
seen.set(k, true);
|
|
61
|
+
const kind = publicByDesign(name, value);
|
|
62
|
+
if (kind) {
|
|
63
|
+
if (!byDesign.has(name)) byDesign.set(name, { kind, hits: [] });
|
|
64
|
+
byDesign.get(name).hits.push(`${file}:${i + 1}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out.push({
|
|
68
|
+
category: 'secret', severity: severityOf(name), confidence: 'high', key: `public-env:${name}`, tool: 'vsg', ruleId: 'public-env-secret',
|
|
69
|
+
location: { file, line: i + 1 },
|
|
70
|
+
title: `Secret-named variable ${name} is exposed to the browser`,
|
|
71
|
+
description: `Variables starting with ${/^(?:NEXT_PUBLIC_|EXPO_PUBLIC_|VITE_)/.exec(name)[0]} are compiled into client-side JavaScript that anyone can read. The name ${name} indicates a secret.`,
|
|
72
|
+
evidence: `${name} at ${file} line ${i + 1}`, refs: [],
|
|
73
|
+
fix: `Rename it without the public prefix, read it only in server code, and rotate the credential because it may already be in a shipped bundle. Public keys such as publishable or anon keys are fine to expose.`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Public-by-design keys: one info finding per variable name, not a secret.
|
|
79
|
+
for (const [name, { kind, hits }] of byDesign) {
|
|
80
|
+
out.push({
|
|
81
|
+
category: 'secret', severity: 'info', confidence: 'high', key: `public-by-design:${name}`, tool: 'vsg', ruleId: 'public-env-public-by-design',
|
|
82
|
+
location: { file: hits[0].replace(/:\d+$/, ''), line: Number(/:(\d+)$/.exec(hits[0])[1]) },
|
|
83
|
+
title: `${name} is a ${kind}, public by design`,
|
|
84
|
+
description: `${name} has a secret-looking name but holds a ${kind}, which is meant to be shipped to the browser. It is not a secret. Reported once per variable.`,
|
|
85
|
+
evidence: `${name} at ${hits.slice(0, 10).join(', ')}${hits.length > 10 ? `, and ${hits.length - 10} more` : ''}`, refs: [],
|
|
86
|
+
fix: 'No action needed. Confirm the key is restricted to your domains or origins where the vendor supports it.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- public-by-design values and names (REQ-037) ----
|
|
93
|
+
// A secret-looking name is accepted when the value (when one is on the line) has a known public shape, or when the name belongs to a
|
|
94
|
+
// known public-key family and the value, if present, is not a recognizable secret. Values are inspected here and never stored.
|
|
95
|
+
const RULES = [
|
|
96
|
+
{ kind: 'PostHog project key', name: /POSTHOG/, value: /^phc_[A-Za-z0-9]{16,}$/ },
|
|
97
|
+
{ kind: 'Sentry DSN', name: /SENTRY[A-Z0-9_]*DSN|DSN[A-Z0-9_]*SENTRY/, value: /^https:\/\/[0-9a-f]{16,64}(?::[^@/\s]*)?@[^/\s]+\/\d+$/i },
|
|
98
|
+
{ kind: 'Stripe publishable key', name: /STRIPE[A-Z0-9_]*(?:PUBLISHABLE|_PK)/, value: /^pk_(?:live|test)_[A-Za-z0-9]{8,}$/ },
|
|
99
|
+
{ kind: 'Supabase anon key', name: /SUPABASE[A-Z0-9_]*ANON/, value: (v) => supabasePublicKeyKind(v) === 'anon key' },
|
|
100
|
+
{ kind: 'Supabase publishable key', name: /SUPABASE[A-Z0-9_]*PUBLISHABLE/, value: (v) => supabasePublicKeyKind(v) === 'publishable key' },
|
|
101
|
+
{ kind: 'Google Maps browser key', name: /(?:GOOGLE|GMAPS|MAPS)[A-Z0-9_]*(?:KEY|TOKEN)/, value: /^AIza[0-9A-Za-z_-]{30,}$/ },
|
|
102
|
+
{ kind: 'Mapbox public token', name: /MAPBOX/, value: /^pk\.[A-Za-z0-9._-]{20,}$/ },
|
|
103
|
+
];
|
|
104
|
+
const matchesValue = (rule, v) => (typeof rule.value === 'function' ? rule.value(v) : rule.value.test(v));
|
|
105
|
+
/** A value that is clearly a private credential (so a public-looking name must not excuse it). */
|
|
106
|
+
const looksPrivate = (v) => /^(?:sk[_.-]|rk_|ghp_|gho_|github_pat_|AKIA|xox[abprs]-|phx_|sb_secret_)/.test(v) || (!!jwtClaims(v) && !supabasePublicKeyKind(v));
|
|
107
|
+
const usable = (v) => !!v && /^[A-Za-z0-9._~+/=:@%-]{8,}$/.test(v) && !/^(?:process|import|env|\$)/.test(v);
|
|
108
|
+
|
|
109
|
+
/** The text after NAME on a line, as a value: `NAME=value`, `NAME: "value"`. Empty for a bare reference such as process.env.NAME. */
|
|
110
|
+
const valueAfter = (line, from) => /^\s*[:=]\s*["'`]?([^\s"'`,;)}\]]+)/.exec(line.slice(from))?.[1] ?? '';
|
|
111
|
+
|
|
112
|
+
/** The kind of public-by-design key a variable holds ('' when it is not one). */
|
|
113
|
+
export function publicByDesign(name, value = '') {
|
|
114
|
+
if (usable(value)) {
|
|
115
|
+
const byValue = RULES.find((r) => matchesValue(r, value));
|
|
116
|
+
if (byValue) return byValue.kind;
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
return RULES.find((r) => r.name.test(name))?.kind ?? '';
|
|
120
|
+
}
|