@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,1201 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { AREAS } from '../agent/schema.mjs';
|
|
6
|
+
import { buildLatticeRuntimeSource } from './runtime.mjs';
|
|
7
|
+
|
|
8
|
+
const FORMAT = 'lattice-security-artifact-v1';
|
|
9
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
10
|
+
const ORIGINAL_SOURCE_ARCHIVE_URL = /^https:\/\/registry\.npmjs\.org\/(?:@latticeai\/cli\/-\/cli-|@m2-ai\/lattice-cli\/-\/lattice-cli-)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.tgz(?![\s\S])/;
|
|
11
|
+
const AGENT_REVIEW_AREA_METADATA = {
|
|
12
|
+
'access-control': {
|
|
13
|
+
label: 'Access control',
|
|
14
|
+
summary: 'A customer-agent review placed this finding in the access-control area. The review narrative and proposed change remain local to the VSG report.',
|
|
15
|
+
},
|
|
16
|
+
supabase: {
|
|
17
|
+
label: 'Supabase data access',
|
|
18
|
+
summary: 'A customer-agent review placed this finding in the Supabase area. The review narrative and proposed change remain local to the VSG report.',
|
|
19
|
+
},
|
|
20
|
+
payments: {
|
|
21
|
+
label: 'Payments',
|
|
22
|
+
summary: 'A customer-agent review placed this finding in the payments area. The review narrative and proposed change remain local to the VSG report.',
|
|
23
|
+
},
|
|
24
|
+
'webhooks-integrations': {
|
|
25
|
+
label: 'Webhooks and integrations',
|
|
26
|
+
summary: 'A customer-agent review placed this finding in the webhooks and integrations area. The review narrative and proposed change remain local to the VSG report.',
|
|
27
|
+
},
|
|
28
|
+
'input-handling': {
|
|
29
|
+
label: 'Input handling',
|
|
30
|
+
summary: 'A customer-agent review placed this finding in the input-handling area. The review narrative and proposed change remain local to the VSG report.',
|
|
31
|
+
},
|
|
32
|
+
'secrets-config': {
|
|
33
|
+
label: 'Secrets and configuration',
|
|
34
|
+
summary: 'A customer-agent review placed this finding in the secrets and configuration area. The review narrative and proposed change remain local to the VSG report.',
|
|
35
|
+
},
|
|
36
|
+
'ai-features': {
|
|
37
|
+
label: 'AI features',
|
|
38
|
+
summary: 'A customer-agent review placed this finding in the AI-features area. The review narrative and proposed change remain local to the VSG report.',
|
|
39
|
+
},
|
|
40
|
+
'privacy-account-recovery': {
|
|
41
|
+
label: 'Privacy and account recovery',
|
|
42
|
+
summary: 'A customer-agent review placed this finding in the privacy and account-recovery area. The review narrative and proposed change remain local to the VSG report.',
|
|
43
|
+
},
|
|
44
|
+
'mobile-expo': {
|
|
45
|
+
label: 'Mobile and Expo',
|
|
46
|
+
summary: 'A customer-agent review placed this finding in the mobile and Expo area. The review narrative and proposed change remain local to the VSG report.',
|
|
47
|
+
},
|
|
48
|
+
'desktop-python': {
|
|
49
|
+
label: 'Desktop and Python',
|
|
50
|
+
summary: 'A customer-agent review placed this finding in the desktop and Python area. The review narrative and proposed change remain local to the VSG report.',
|
|
51
|
+
},
|
|
52
|
+
'supply-chain': {
|
|
53
|
+
label: 'Software supply chain',
|
|
54
|
+
summary: 'A customer-agent review placed this finding in the software supply-chain area. The review narrative and proposed change remain local to the VSG report.',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const BETTERLEAKS_RULE_CONTRACTS = [
|
|
58
|
+
{
|
|
59
|
+
ruleId: 'generic-api-key',
|
|
60
|
+
evidencePath: 'licensing/candidates/betterleaks-generic-api-key.json',
|
|
61
|
+
evidenceSha256: 'fa88c2b065ef0876b59eb488814983efe23c24eff90dac1a272aeb30e955096c',
|
|
62
|
+
sourceSha256: '89afa6d460ea97d7ca2330433b839b7aedf6afad83251ec40cc35b813e42c57a',
|
|
63
|
+
originSourceSha256: '7ffad1b698b30865fabbe9c8d0b8ac9ebb9606a255bac591b81e63304c0264d4',
|
|
64
|
+
initialImportSha256: '47a3216fd96cb2b89e3405e3a12b2e8572b0a0b834b72e2399289b30f40ca9cd',
|
|
65
|
+
attributionIds: ['betterleaks:generic-api-key', 'gitleaks:generic-api-key-origin'],
|
|
66
|
+
missingBlocker: 'betterleaks_generic_api_key_provenance_missing',
|
|
67
|
+
summary: 'A value matched the pinned Betterleaks generic API key pattern. Treat it as a possible credential until its issuing service and privileges are confirmed.',
|
|
68
|
+
remediation: 'Identify the issuing service. If the value is a real credential, revoke or rotate it, remove it from source and history, and load the replacement from a secret manager or private environment variable. If it is test data or a false positive, remove or replace the credential-shaped value, or use VSG acceptance only after documented review.',
|
|
69
|
+
retest: 'Run the same VSG secrets check again and confirm the finding is gone or explicitly accepted through the existing review workflow.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ruleId: 'jwt',
|
|
73
|
+
evidencePath: 'licensing/candidates/betterleaks-jwt.json',
|
|
74
|
+
evidenceSha256: 'b0f411643bcc491ebb572b18387456bfe9ccf77c5a050b0a59d05d5a60e3c44f',
|
|
75
|
+
sourceSha256: '3d217c02a191f5d4f915bab9c0f486d2c297a694eb00fba58c13d4a4392276e7',
|
|
76
|
+
originSourceSha256: '487025fbac8fef09c102bbfe35ba686df70c274d347f534b9d0aeb69259829e5',
|
|
77
|
+
initialImportSha256: '7cc52e8ab224c5ecd371b47cefb2ff87d39f3f6a0ac91a462ed7ad1e8027593e',
|
|
78
|
+
attributionIds: ['betterleaks:jwt', 'gitleaks:jwt-origin'],
|
|
79
|
+
missingBlocker: 'betterleaks_jwt_provenance_missing',
|
|
80
|
+
summary: 'A value matched the pinned Betterleaks JSON Web Token pattern. Treat it as a possible bearer credential until its issuer, intended audience, expiry, and privileges are confirmed.',
|
|
81
|
+
remediation: 'Identify the token issuer and privileges. If the token is active or may grant access, revoke or rotate its signing or session credentials as appropriate, remove it from source and history, and load replacements from a secret manager or private environment variable.',
|
|
82
|
+
retest: 'Run the same VSG secrets check again and confirm the token is gone or explicitly accepted through the existing review workflow.',
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const BETTERLEAKS_NOTICE_PATHS = [
|
|
86
|
+
'licensing/notices/betterleaks-1.9.0-MIT.txt',
|
|
87
|
+
'licensing/notices/gitleaks-generic-origin-782f3104-MIT.txt',
|
|
88
|
+
];
|
|
89
|
+
const QUALIFICATION_RECORDS = [
|
|
90
|
+
{ path: 'licensing/candidates/commercial-rule-candidate.json',
|
|
91
|
+
sha256: '2b305facf86c308dee3131dbc9854abf5291e29546d4fd641bace4bb1b920a9c' },
|
|
92
|
+
{ path: 'licensing/candidates/gitlab-sast-source-notice-bundle.json',
|
|
93
|
+
sha256: '95494e6442be48932743220de45124fb51dc578eddb8d72174f1038c6a2e4011' },
|
|
94
|
+
];
|
|
95
|
+
const SELECTED_RUNTIME_RECORDS = Object.freeze({
|
|
96
|
+
sastSourceEvidence: { path: 'licensing/candidates/gitlab-sast-source-evidence.json', sha256: '662c47108fe1acfd6ce3db5508fcfe81bb851f666bb5d4091e80d1f35ace0e91' },
|
|
97
|
+
sastIdentities: { path: 'licensing/candidates/gitlab-sast-rule-identities.json', sha256: '533f9211267df50f004cbb38deb49abbb210a23d6acbe7a6f11379fbbb24b1b2' },
|
|
98
|
+
yaraInventory: { path: 'licensing/candidates/yara-forge-20260927.json', sha256: '4a1eb672e488b99d3381979a9eb1de4890edee79cfbcded9502683d48dbc7d54' },
|
|
99
|
+
coverage: { path: 'licensing/candidates/selected-runtime-coverage.json', sha256: '92daa13869cd402c64da9d2e6e6fd6198ffd58587d4d2080804c821514f46a2d' },
|
|
100
|
+
});
|
|
101
|
+
const SELECTED_NOTICE_RECORDS = Object.freeze({
|
|
102
|
+
malpedia: { path: 'licensing/candidates/malpedia-source-license-evidence.json', sha256: '4729a9374e28fd1ac463860efa7b596269cb323b97488de4d27af53506940e50' },
|
|
103
|
+
apache: { path: 'licensing/candidates/apache-source-license-evidence.json', sha256: 'ab29e41e05a2c2dc333b2fbf05790c7497ba34a6aec125ed9a33f27756124350' },
|
|
104
|
+
cado: { path: 'licensing/candidates/cado-source-declaration-evidence.json', sha256: '810cadb12983588a7001fee92a328fb6ac66e9e766eb90ed06d4f083ad341e36' },
|
|
105
|
+
blackberry: { path: 'licensing/candidates/blackberry-source-declaration-evidence.json', sha256: '662602c01d6cc755ace812570591d8257de3462562e0ca4b57fa33c3a1ff7e8e' },
|
|
106
|
+
blackberryAdditional: { path: 'licensing/candidates/blackberry-additional-source-declaration-evidence.json', sha256: 'db922d1ab580eb09a01de5db83741f134a4446610a0762a4c91a0c298099de2f' },
|
|
107
|
+
});
|
|
108
|
+
const COMMERCIAL_RELEASE_RECORD = Object.freeze({
|
|
109
|
+
path: 'licensing/candidates/commercial-runtime-release.json',
|
|
110
|
+
});
|
|
111
|
+
const COMMERCIAL_RELEASE_EVIDENCE = Object.freeze([
|
|
112
|
+
...QUALIFICATION_RECORDS,
|
|
113
|
+
SELECTED_RUNTIME_RECORDS.coverage,
|
|
114
|
+
{ path: 'licensing/candidates/rule-license-evidence.json', sha256: '436466392799976cd3dd7788e3f28700b9c7828ac0586adc2979c57c7f239572' },
|
|
115
|
+
{ path: 'licensing/candidates/runner-image-composition.json', sha256: '2bbb1193ab1e1a92a68f90e0e5fef1759da4030990392c819a4eb05d31fdeddd' },
|
|
116
|
+
{ path: 'licensing/candidates/data-feed-evidence.json', sha256: '014af19a07a02e36063bdb140fcea627d09ef8c7bc90ea12c365bd3736a91042' },
|
|
117
|
+
]);
|
|
118
|
+
const TOP_LEVEL_FILES = new Set([
|
|
119
|
+
'LICENSE', 'NOTICE', 'package.json', 'licensing/commercial-components.json',
|
|
120
|
+
...BETTERLEAKS_RULE_CONTRACTS.map((item) => item.evidencePath), ...BETTERLEAKS_NOTICE_PATHS,
|
|
121
|
+
...QUALIFICATION_RECORDS.map((item) => item.path),
|
|
122
|
+
...Object.values(SELECTED_RUNTIME_RECORDS).map((item) => item.path),
|
|
123
|
+
...Object.values(SELECTED_NOTICE_RECORDS).map((item) => item.path),
|
|
124
|
+
COMMERCIAL_RELEASE_RECORD.path,
|
|
125
|
+
...COMMERCIAL_RELEASE_EVIDENCE.map((item) => item.path),
|
|
126
|
+
]);
|
|
127
|
+
const SOURCE_PREFIXES = ['bin/', 'src/', 'rules/', 'playbook/', 'tools/registry/', 'licensing/notices/candidate-sources/sha256/'];
|
|
128
|
+
const RULE_TEST_SPECIMEN = /^rules\/vsg-[^/]+\.(?:ts|tsx|js|py)$/i;
|
|
129
|
+
const FORBIDDEN_PARTS = new Set(['.git', '.vsg', 'node_modules', '.cache', 'cache', 'reports', 'raw', 'bin-cache']);
|
|
130
|
+
const ENTRY_POINT = 'bin/vsg.mjs';
|
|
131
|
+
const EXPORTER_PATH = 'src/lattice/export.mjs';
|
|
132
|
+
const TRANSPORT_PATH = 'src/lattice/transport.mjs';
|
|
133
|
+
const EVALUATOR_PATH = 'lattice-runtime.mjs';
|
|
134
|
+
const RELEASE_BLOCKERS = [
|
|
135
|
+
'commercial_component_rights_not_cleared',
|
|
136
|
+
'third_party_attribution_catalog_incomplete',
|
|
137
|
+
'replacement_rule_coverage_not_approved',
|
|
138
|
+
'artifact_release_approval_missing',
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const posix = (value) => value.split(path.sep).join('/');
|
|
142
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
143
|
+
const canonical = (value) => {
|
|
144
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
145
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
146
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(',')}}`;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function fail(message) {
|
|
150
|
+
throw new Error(`Lattice artifact rejected: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function validateOriginalSourceArchiveUrl(value) {
|
|
154
|
+
if (value !== undefined && (typeof value !== 'string' || !ORIGINAL_SOURCE_ARCHIVE_URL.test(value))) {
|
|
155
|
+
fail('originalSourceArchiveUrl must be a canonical stable-version public @latticeai/cli or @m2-ai/lattice-cli npm tarball URL.');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeRelative(rel) {
|
|
160
|
+
return typeof rel === 'string' && rel.length > 0 && !/[\\:]/.test(rel) && rel === posix(rel) && !path.posix.isAbsolute(rel)
|
|
161
|
+
&& rel.split('/').every((part) => part && part !== '.' && part !== '..' && !FORBIDDEN_PARTS.has(part.toLowerCase()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function git(sourceRoot, args, options = {}) {
|
|
165
|
+
try {
|
|
166
|
+
return execFileSync('git', ['-C', sourceRoot, ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, ...options }).trim();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
fail(`Git could not verify the source (${error.message}).`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hashDirectory(sourceRoot, prefix, tracked) {
|
|
173
|
+
const hash = crypto.createHash('sha256');
|
|
174
|
+
for (const rel of tracked.filter((item) => item.startsWith(prefix)).sort()) {
|
|
175
|
+
hash.update(rel.slice(prefix.length));
|
|
176
|
+
hash.update('\0');
|
|
177
|
+
hash.update(fs.readFileSync(path.join(sourceRoot, ...rel.split('/'))));
|
|
178
|
+
hash.update('\0');
|
|
179
|
+
}
|
|
180
|
+
return hash.digest('hex');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function trackedFiles(sourceRoot) {
|
|
184
|
+
const raw = execFileSync('git', ['-C', sourceRoot, 'ls-files', '-z'], { encoding: 'buffer', maxBuffer: 32 * 1024 * 1024 });
|
|
185
|
+
return raw.toString('utf8').split('\0').filter(Boolean).map(posix);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sourceAllowlist(sourceRoot, tracked) {
|
|
189
|
+
const files = tracked.filter((rel) => !RULE_TEST_SPECIMEN.test(rel)
|
|
190
|
+
&& (TOP_LEVEL_FILES.has(rel) || SOURCE_PREFIXES.some((prefix) => rel.startsWith(prefix)))).sort();
|
|
191
|
+
for (const required of [ENTRY_POINT, EXPORTER_PATH, TRANSPORT_PATH, 'src/core/gate.mjs', 'package.json', 'LICENSE', 'NOTICE', 'licensing/commercial-components.json']) {
|
|
192
|
+
if (!files.includes(required)) fail(`required source file ${required} is missing.`);
|
|
193
|
+
}
|
|
194
|
+
for (const rel of files) {
|
|
195
|
+
if (!safeRelative(rel)) fail(`unsafe source path ${rel}.`);
|
|
196
|
+
const stat = fs.lstatSync(path.join(sourceRoot, ...rel.split('/')));
|
|
197
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`source path ${rel} is not a regular file.`);
|
|
198
|
+
}
|
|
199
|
+
return files;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function yamlScalar(value) {
|
|
203
|
+
const text = value.trim();
|
|
204
|
+
if (text.startsWith('"')) {
|
|
205
|
+
try { return JSON.parse(text); } catch { fail(`invalid quoted rule metadata ${text}.`); }
|
|
206
|
+
}
|
|
207
|
+
if (text.startsWith("'") && text.endsWith("'")) return text.slice(1, -1).replace(/''/g, "'");
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function foldedValue(lines, start, indent) {
|
|
212
|
+
const parts = [];
|
|
213
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
214
|
+
const line = lines[index];
|
|
215
|
+
if (line.trim() && line.match(/^ */)[0].length <= indent) break;
|
|
216
|
+
if (line.trim()) parts.push(line.trim());
|
|
217
|
+
}
|
|
218
|
+
return parts.join(' ');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function originalRules(sourceRoot, tracked) {
|
|
222
|
+
const rules = {};
|
|
223
|
+
const provenance = {};
|
|
224
|
+
for (const rel of tracked.filter((item) => /^rules\/vsg-[^/]+\.ya?ml$/.test(item)).sort()) {
|
|
225
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...rel.split('/')));
|
|
226
|
+
const lines = bytes.toString('utf8').split(/\r?\n/);
|
|
227
|
+
for (let start = 0; start < lines.length; start++) {
|
|
228
|
+
const idMatch = /^ - id:\s*(\S+)\s*$/.exec(lines[start]);
|
|
229
|
+
if (!idMatch) continue;
|
|
230
|
+
const end = lines.findIndex((line, index) => index > start && /^ - id:\s*/.test(line));
|
|
231
|
+
const block = lines.slice(start, end < 0 ? lines.length : end);
|
|
232
|
+
const scalar = (name) => {
|
|
233
|
+
const index = block.findIndex((line) => new RegExp(`^ {4,8}${name}:\\s*`).test(line));
|
|
234
|
+
if (index < 0) return '';
|
|
235
|
+
const raw = block[index].replace(new RegExp(`^ {4,8}${name}:\\s*`), '');
|
|
236
|
+
return /^[>|][+-]?$/.test(raw.trim()) ? foldedValue(block, index, block[index].match(/^ */)[0].length) : yamlScalar(raw);
|
|
237
|
+
};
|
|
238
|
+
const ruleId = idMatch[1];
|
|
239
|
+
const title = scalar('vsg-title');
|
|
240
|
+
const summary = scalar('message');
|
|
241
|
+
const remediation = scalar('vsg-fix');
|
|
242
|
+
const cwe = /CWE-[1-9][0-9]{0,5}/i.exec(scalar('cwe'))?.[0]?.toUpperCase();
|
|
243
|
+
if (!title || !summary || !remediation) fail(`original rule ${ruleId} lacks trusted presentation metadata.`);
|
|
244
|
+
rules[ruleId] = {
|
|
245
|
+
ruleId, title, summary, remediation,
|
|
246
|
+
retest: `Run VSG rule ${ruleId} again after applying the repair.`,
|
|
247
|
+
attributionIds: ['m2ai:vsg-original'],
|
|
248
|
+
};
|
|
249
|
+
provenance[ruleId] = { sourcePath: rel, sourceSha256: sha256(bytes), cweIds: cwe ? [cwe] : [] };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { rules, provenance };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function builtinRules(sourceRoot, tracked) {
|
|
256
|
+
const rules = {};
|
|
257
|
+
const provenance = {};
|
|
258
|
+
const sources = [
|
|
259
|
+
{
|
|
260
|
+
rel: 'src/checks/_lib/secrets.mjs',
|
|
261
|
+
definitions: [
|
|
262
|
+
{
|
|
263
|
+
ruleId: 'supabase-public-key',
|
|
264
|
+
title: 'Supabase public key',
|
|
265
|
+
summary: 'A Supabase anon or publishable API key is intended for public clients and does not grant elevated access. Keep Row Level Security and least-privilege grants enabled.',
|
|
266
|
+
remediation: 'No secret rotation is needed for this public key. Confirm the project uses Row Level Security and least-privilege grants.',
|
|
267
|
+
retest: 'Run the VSG secrets check again and confirm the key remains classified as a public Supabase key.',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
ruleId: 'supabase-demo-jwt',
|
|
271
|
+
title: 'Supabase local demo key',
|
|
272
|
+
summary: 'A JWT whose issuer is "supabase-demo" is the public development key every local Supabase stack ships with. It unlocks nothing outside a local stack, so it is not a secret. Reported once.',
|
|
273
|
+
remediation: 'No action needed. Make sure no real project key sits next to it.',
|
|
274
|
+
retest: 'Run the VSG secrets check again and confirm only the local Supabase demo key is reported.',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
rel: 'src/checks/secrets-tree/env.mjs',
|
|
280
|
+
definitions: [
|
|
281
|
+
{
|
|
282
|
+
ruleId: 'committed-env-file',
|
|
283
|
+
title: 'Environment file exposed to the repository',
|
|
284
|
+
summary: 'An environment file can contain private configuration or credentials. A committed file remains in repository history; an unignored file can be added by a later commit.',
|
|
285
|
+
remediation: 'Keep credentials in private environment variables or a secret manager. Add the file to .gitignore, remove committed copies from the repository, rotate credentials they contained, and keep only placeholders in an example file.',
|
|
286
|
+
retest: 'Run the VSG secrets check again and confirm the environment file is ignored or contains placeholders only.',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
ruleId: 'public-env-secret',
|
|
290
|
+
title: 'Secret-named variable exposed to the browser',
|
|
291
|
+
summary: 'A variable with a public browser prefix has a name that indicates a secret. The finding contains the variable name and location, not its value.',
|
|
292
|
+
remediation: 'Rename the variable without the public prefix and read it only in server code. Rotate the credential because it may already be present in a shipped browser bundle.',
|
|
293
|
+
retest: 'Run the VSG secrets check again and confirm no secret-named public variable remains.',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
ruleId: 'public-env-public-by-design',
|
|
297
|
+
title: 'Public-by-design browser key',
|
|
298
|
+
summary: 'The variable matches a known public browser key or DSN shape and is intended for client exposure. It is not treated as a secret by this check.',
|
|
299
|
+
remediation: 'No secret rotation is needed for a public-by-design key. Confirm provider-side origin restrictions and least-privilege settings where supported.',
|
|
300
|
+
retest: 'Run the VSG secrets check again and confirm the value remains classified as public by design.',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
rel: 'src/checks/deps-vuln/index.mjs',
|
|
306
|
+
definitions: [
|
|
307
|
+
['unpinned-requirements', 'Python requirement is not pinned', 'An unpinned Python requirement can resolve to a different version on each install, so vulnerability matching cannot identify the installed version reliably.', 'Pin each requirement to an exact version or use a lock file, then rerun the dependency check.'],
|
|
308
|
+
['enrichment-degraded', 'Exploit data could not be fully checked', 'The vulnerability scan completed, but one or more external exploit-data sources could not be checked.', 'Rerun when the data sources are available and review the check coverage before release.'],
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
rel: 'src/checks/desktop-config/index.mjs',
|
|
313
|
+
definitions: [
|
|
314
|
+
['vsg/desktop/update-unverified', 'Desktop updater has no visible integrity check', 'An updater downloads content and runs or replaces files without a visible signature or hash check in the surrounding code.', 'Verify downloaded updates with a detached signature or pinned hash before running or extracting them, and refuse older versions.'],
|
|
315
|
+
['vsg/desktop/binaries-scanned', 'Desktop build binaries were included in the malware check', 'Windows executables or installers in build output or configured artifacts were included in the malware scan.', 'Confirm the malware check completed against the exact files intended for release.'],
|
|
316
|
+
['vsg/desktop/no-code-signing', 'No Windows code-signing step was found', 'No supported code-signing command or action was found in the packaging scripts or CI files.', 'Sign the executable and installer in the build and keep signing keys in a secrets store.'],
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
rel: 'src/checks/install-posture/index.mjs',
|
|
321
|
+
definitions: [
|
|
322
|
+
['install-posture/firewall-present', 'CI install uses a dependency firewall', 'A CI workflow that installs dependencies also references Aikido Safe Chain or Socket Firewall.', 'Keep the firewall before every package installation step.'],
|
|
323
|
+
['install-posture/firewall-absent', 'CI install has no dependency firewall', 'A CI workflow installs dependencies without an Aikido Safe Chain or Socket Firewall check.', 'Add a dependency firewall before package installation in the workflow.'],
|
|
324
|
+
['install-posture/pnpm-allow-all-builds', 'pnpm allows every dependency build script', 'dangerouslyAllowAllBuilds permits every dependency to run its install-time build script.', 'Remove dangerouslyAllowAllBuilds and allow only the packages that need build scripts.'],
|
|
325
|
+
['install-posture/pnpm-no-only-built', 'pnpm build scripts have no explicit allowlist', 'The lockfile has packages with build scripts, but no onlyBuiltDependencies allowlist was found.', 'Add onlyBuiltDependencies listing the packages that need a build step.'],
|
|
326
|
+
['install-posture/npm-ignore-scripts-set', 'npm install scripts are disabled', 'The project configures npm to skip dependency lifecycle scripts during install.', 'No action is needed for this finding; run required build scripts explicitly after reviewing them.'],
|
|
327
|
+
['install-posture/npm-ignore-scripts-unset', 'npm install scripts are enabled', 'npm can run install-time scripts from every dependency during installation.', 'Set ignore-scripts=true in .npmrc and run required build scripts explicitly after review.'],
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
rel: 'src/checks/supabase-static/index.mjs',
|
|
332
|
+
definitions: [
|
|
333
|
+
['supabase-rls-disabled', 'Supabase table has row level security disabled', 'A public-schema table is reachable through the Supabase API without row-level policies restricting access.', 'Enable row level security and add policies that restrict rows to the intended users and operations.'],
|
|
334
|
+
['supabase-storage-policy-open', 'Supabase storage policy grants broad access', 'A storage.objects policy uses an unconditional expression and does not restrict objects by bucket or owner.', 'Replace the policy with one scoped to the intended bucket and authenticated owner.'],
|
|
335
|
+
['supabase-policy-unconditional', 'Supabase policy allows access to every row', 'A row-level security policy uses a literal true condition for an exposed role.', 'Replace the policy with an owner or tenant condition, or remove the policy if the table should be private.'],
|
|
336
|
+
['supabase-definer-search-path', 'Security definer function has no fixed search path', 'A security definer function without a fixed search_path may resolve attacker-controlled objects while running with its owner privileges.', 'Set search_path to an empty value and schema-qualify every referenced object.'],
|
|
337
|
+
['supabase-view-security-invoker', 'Supabase view may bypass row level security', 'A public view runs with its owner privileges because security_invoker is not enabled.', 'Set security_invoker=true and verify that the underlying row-level policies match the intended access.'],
|
|
338
|
+
['supabase-public-bucket', 'Supabase storage bucket is public', 'A public bucket allows anyone with an object URL to download its files without signing in.', 'Make the bucket private and use signed URLs or owner-scoped storage policies unless its contents are intentionally public.'],
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
rel: 'src/checks/mobile-config/index.mjs',
|
|
343
|
+
definitions: [
|
|
344
|
+
{ ruleId: 'vsg/mobile/updates-unsigned', sourceMarker: 'updates-unsigned', title: 'Expo updates are not code-signed', summary: 'The app uses expo-updates but the configuration has no update signing certificate.', remediation: 'Configure update code signing and sign each published update while keeping the private key out of the repository.', retest: 'Run the mobile configuration check again and confirm published updates are signed.' },
|
|
345
|
+
{ ruleId: 'vsg/mobile/android-cleartext', sourceMarker: 'android-cleartext', title: 'Android cleartext traffic is allowed', summary: 'The Android configuration permits plain HTTP traffic that can be read or changed on the network.', remediation: 'Disable cleartext traffic and use HTTPS endpoints.', retest: 'Run the mobile configuration check again and confirm cleartext traffic is disabled.' },
|
|
346
|
+
{ ruleId: 'vsg/mobile/ios-ats-disabled', sourceMarker: 'ios-ats-disabled', title: 'iOS App Transport Security is disabled', summary: 'The iOS configuration allows arbitrary network loads instead of requiring secure transport.', remediation: 'Remove NSAllowsArbitraryLoads and use HTTPS for every endpoint.', retest: 'Run the mobile configuration check again and confirm App Transport Security is enabled.' },
|
|
347
|
+
{ ruleId: 'vsg/mobile/ios-ats-exceptions', sourceMarker: 'ios-ats-exceptions', title: 'iOS App Transport Security has exception domains', summary: 'The iOS configuration weakens transport requirements for one or more listed hosts.', remediation: 'Remove each exception after the host supports modern TLS and review any remaining exception before release.', retest: 'Run the mobile configuration check again and review each remaining exception.' },
|
|
348
|
+
{ ruleId: 'vsg/mobile/permission-READ_SMS', sourceMarker: 'READ_SMS', title: 'App requests permission to read text messages', summary: 'The Android app requests access to SMS messages, which can expose private communications.', remediation: 'Remove the permission unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
349
|
+
{ ruleId: 'vsg/mobile/permission-RECEIVE_SMS', sourceMarker: 'RECEIVE_SMS', title: 'App requests permission to receive text messages', summary: 'The Android app can receive SMS messages, which may expose authentication codes or private communications.', remediation: 'Remove the permission unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
350
|
+
{ ruleId: 'vsg/mobile/permission-READ_CALL_LOG', sourceMarker: 'READ_CALL_LOG', title: 'App requests permission to read call history', summary: 'The Android app requests access to the user call log.', remediation: 'Remove the permission unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
351
|
+
{ ruleId: 'vsg/mobile/permission-SYSTEM_ALERT_WINDOW', sourceMarker: 'SYSTEM_ALERT_WINDOW', title: 'App requests permission to draw over other apps', summary: 'The Android app can display overlays above other applications, a capability commonly used in overlay attacks.', remediation: 'Remove the permission unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
352
|
+
{ ruleId: 'vsg/mobile/permission-REQUEST_INSTALL_PACKAGES', sourceMarker: 'REQUEST_INSTALL_PACKAGES', title: 'App requests permission to install other packages', summary: 'The Android app can prompt to install additional applications.', remediation: 'Remove the permission unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
353
|
+
{ ruleId: 'vsg/mobile/permission-MANAGE_EXTERNAL_STORAGE', sourceMarker: 'MANAGE_EXTERNAL_STORAGE', title: 'App requests broad shared-storage access', summary: 'The Android app requests access to all files in shared storage.', remediation: 'Use scoped storage or a narrower file picker permission where possible.', retest: 'Run the mobile configuration check again and confirm only the necessary storage access remains.' },
|
|
354
|
+
{ ruleId: 'vsg/mobile/permission-ACCESS_BACKGROUND_LOCATION', sourceMarker: 'ACCESS_BACKGROUND_LOCATION', title: 'App requests background location access', summary: 'The Android app can read location while it is in the background.', remediation: 'Remove background location access unless a reviewed feature requires it.', retest: 'Run the mobile configuration check again and confirm the permission is removed or justified.' },
|
|
355
|
+
{ ruleId: 'vsg/mobile/deep-link-schemes', sourceMarker: 'deep-link-schemes', title: 'App declares custom deep-link schemes', summary: 'Custom URL schemes can be claimed by another app, so incoming parameters must be treated as untrusted.', remediation: 'Prefer verified HTTPS app links and validate parameters before sensitive actions.', retest: 'Run the mobile configuration check again and review each deep-link handler.' },
|
|
356
|
+
{ ruleId: 'vsg/mobile/intent-filters', sourceMarker: 'intent-filters', title: 'App declares Android intent filters', summary: 'Intent filters expose app entry points to links and other applications.', remediation: 'Validate incoming data and use verified app links where supported.', retest: 'Run the mobile configuration check again and review each intent handler.' },
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
rel: 'src/checks/dep-change/index.mjs',
|
|
361
|
+
definitions: [
|
|
362
|
+
{ ruleId: 'dep-change/version-removed', sourceMarker: "key: 'version-removed'", title: 'Dependency version was removed from its registry', summary: 'A locked dependency version is no longer available from its registry.', remediation: 'Treat the package as untrusted, review registry advisories, and pin a version that remains available.' },
|
|
363
|
+
{ ruleId: 'dep-change/provenance-dropped', sourceMarker: "key: 'provenance-dropped'", title: 'Dependency release lost build provenance', summary: 'A new release no longer carries the provenance record present in the prior release.', remediation: 'Pin the last reviewed version until the maintainer explains the provenance change.' },
|
|
364
|
+
{ ruleId: 'dep-change/fresh-risky-change', sourceMarker: "key: 'fresh-risky-change'", title: 'New dependency release combines freshness with a risky change', summary: 'A very recent release also adds an install script or a recently created dependency.', remediation: 'Wait for review or pin the previous version, and inspect any new install scripts.' },
|
|
365
|
+
{ ruleId: 'dep-change/published-recently', sourceMarker: "['published-recently'", title: 'Dependency release was published recently', summary: 'A package release is less than 72 hours old.', remediation: 'Let the release age or review its changes before adopting it.' },
|
|
366
|
+
{ ruleId: 'dep-change/publisher-changed', sourceMarker: "['publisher-changed'", title: 'Dependency publisher changed', summary: 'The package registry reports a publisher change for this release.', remediation: 'Confirm the new publisher is a legitimate maintainer before upgrading.' },
|
|
367
|
+
{ ruleId: 'dep-change/new-install-script', sourceMarker: "['new-install-script'", title: 'Dependency release adds an install script', summary: 'The new release adds an install-time script that may execute during installation.', remediation: 'Review the script and consider disabling install scripts for this dependency.' },
|
|
368
|
+
{ ruleId: 'dep-change/new-dependencies', sourceMarker: "['new-dependencies'", title: 'Dependency release adds dependencies', summary: 'The new release introduces one or more dependencies that were not present before.', remediation: 'Review the new dependencies and their release history before upgrading.' },
|
|
369
|
+
{ ruleId: 'dep-change/low-reputation', sourceMarker: "['low-reputation'", title: 'New dependency has low registry reputation', summary: 'A newly added dependency has limited download history or other low-reputation signals.', remediation: 'Confirm the package identity and that the dependency is required.' },
|
|
370
|
+
{ ruleId: 'dep-change/yanked-release', sourceMarker: "['yanked-release'", title: 'Dependency release was yanked', summary: 'The package registry marks the selected dependency release as yanked.', remediation: 'Upgrade to a release that has not been yanked.' },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
rel: 'src/checks/agent-surface/unicode.mjs',
|
|
375
|
+
definitions: [
|
|
376
|
+
['tag-characters', 'Hidden Unicode tag characters', 'Unicode tag characters can be invisible in editors while remaining readable to language models.', 'Delete the characters and review how they entered the repository.'],
|
|
377
|
+
['bidi-controls', 'Hidden bidirectional control characters', 'Bidirectional controls can make displayed text differ from the underlying order of text.', 'Delete the control characters.'],
|
|
378
|
+
['zero-width', 'Hidden zero-width characters', 'Zero-width characters can hide text or split keywords while remaining invisible to reviewers.', 'Delete the characters unless they are intentional and documented.'],
|
|
379
|
+
['variation-selectors', 'Hidden variation selectors', 'Variation selectors outside emoji sequences can encode hidden data.', 'Delete the characters unless they are part of an intentional emoji sequence.'],
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
rel: 'src/checks/agent-surface/config.mjs',
|
|
384
|
+
definitions: [
|
|
385
|
+
['autorun-fetch-exec', 'Auto-run configuration downloads and executes content', 'A developer tool auto-run configuration can fetch content and execute it without an explicit review step.', 'Remove automatic execution or require an explicit, reviewed local action.'],
|
|
386
|
+
['autorun-present', 'Developer tool auto-run configuration is present', 'A developer tool auto-run configuration can run when the repository is opened.', 'Review the configured command and remove it if it is not required.'],
|
|
387
|
+
['mcp-unpinned', 'MCP server package is not pinned', 'An MCP server configuration references a package without an immutable version.', 'Pin the MCP server package to an exact reviewed version.'],
|
|
388
|
+
['mcp-plain-http', 'MCP server uses plain HTTP', 'An MCP server endpoint uses unencrypted HTTP transport.', 'Use HTTPS for the MCP server endpoint.'],
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
rel: 'src/checks/agent-surface/instructions.mjs',
|
|
393
|
+
definitions: [
|
|
394
|
+
['instruction-fetch-exec', 'Agent instructions fetch and execute remote content', 'Repository instructions direct an agent to fetch remote content and run it.', 'Remove the instruction or replace it with a reviewed, locally verifiable procedure.'],
|
|
395
|
+
['instruction-credentials', 'Agent instructions request credential access', 'Repository instructions direct an agent to read credential material.', 'Remove the request and keep credentials in approved secret storage.'],
|
|
396
|
+
['instruction-hidden-markup', 'Agent instructions contain hidden markup', 'Hidden markup can conceal instructions from a human reviewer while remaining available to a parser or agent.', 'Remove the hidden markup and review the surrounding instructions.'],
|
|
397
|
+
['instruction-hidden-comment', 'Agent instructions contain a hidden comment', 'A hidden comment can carry instructions that are not visible in the rendered document.', 'Remove the comment and review the surrounding instructions.'],
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
rel: 'src/live/analyze.mjs',
|
|
402
|
+
definitions: [
|
|
403
|
+
{ ruleId: 'header-strict-transport-security', sourceMarker: "'strict-transport-security':", title: 'HSTS header is missing', summary: 'Without HSTS, a browser can be downgraded to plain HTTP by a network attacker.', remediation: 'Send Strict-Transport-Security on every HTTPS response.' },
|
|
404
|
+
{ ruleId: 'header-content-security-policy', sourceMarker: "'content-security-policy':", title: 'Content Security Policy is missing', summary: 'A Content Security Policy limits what an injected script can do.', remediation: 'Send a policy that restricts script sources and use nonces or hashes for inline scripts.' },
|
|
405
|
+
{ ruleId: 'header-x-content-type-options', sourceMarker: "'x-content-type-options':", title: 'X-Content-Type-Options header is missing', summary: 'Without nosniff, browsers may guess content types and run uploaded files as scripts.', remediation: 'Send X-Content-Type-Options: nosniff.' },
|
|
406
|
+
{ ruleId: 'header-frame-protection', sourceMarker: "'frame-protection':", title: 'Clickjacking protection is missing', summary: 'Another site may embed the page and trick visitors into interacting with hidden controls.', remediation: 'Set frame-ancestors or X-Frame-Options to restrict framing.' },
|
|
407
|
+
{ ruleId: 'header-referrer-policy', sourceMarker: "'referrer-policy':", title: 'Referrer-Policy header is missing', summary: 'Full URLs can leak through the Referer header when visitors navigate to other sites.', remediation: 'Set a Referrer-Policy such as strict-origin-when-cross-origin.' },
|
|
408
|
+
{ ruleId: 'header-permissions-policy', sourceMarker: "'permissions-policy':", title: 'Permissions-Policy header is missing', summary: 'Browser features such as camera, microphone and geolocation are not restricted by a Permissions-Policy.', remediation: 'Disable browser features the site does not use.' },
|
|
409
|
+
{ ruleId: 'header-hsts-short', sourceMarker: "ruleId: 'header-hsts-short'", title: 'HSTS max-age is shorter than 180 days', summary: 'A short HSTS lifetime leaves returning visitors unprotected when it expires.', remediation: 'Set an HSTS max-age of at least 180 days.' },
|
|
410
|
+
{ ruleId: 'header-csp-unsafe-inline', sourceMarker: "ruleId: 'header-csp-unsafe-inline'", title: 'Content Security Policy permits unsafe inline scripts', summary: 'unsafe-inline weakens the protection against injected scripts.', remediation: 'Use a nonce or hash for required inline scripts and remove unsafe-inline.' },
|
|
411
|
+
['cookie-flags', 'Cookie is missing security flags', 'A cookie without the appropriate flags may be exposed to scripts, plain HTTP or cross-site requests.', 'Set Secure and SameSite; also set HttpOnly for session and token cookies.'],
|
|
412
|
+
['cors-reflected-credentials', 'CORS reflects an untrusted origin with credentials', 'A site can read credentialed responses from a visitor browser when the server reflects an arbitrary origin.', 'Allow only explicit trusted origins and never combine broad origin reflection with credentials.'],
|
|
413
|
+
['cors-wildcard', 'CORS allows every origin', 'The Access-Control-Allow-Origin wildcard grants browser access to responses from any origin.', 'Allow only the origins that need access.'],
|
|
414
|
+
['cors-reflected', 'CORS reflects the requested origin', 'The server echoes the requested origin instead of checking it against an explicit allowlist.', 'Allow only explicit trusted origins and reject other Origin values.'],
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
rel: 'src/live/exposure.mjs',
|
|
419
|
+
definitions: [
|
|
420
|
+
{ ruleId: 'exposed-env', sourceMarker: "kind: 'env'", title: 'Environment file is publicly readable', summary: 'A deployed environment file is readable without authentication and can contain private configuration or credentials.', remediation: 'Remove the file from the web root and rotate every value it held.' },
|
|
421
|
+
{ ruleId: 'exposed-vcs', sourceMarker: "kind: 'vcs'", title: 'Version control metadata is publicly readable', summary: 'A deployed version control metadata file is readable without authentication.', remediation: 'Remove repository metadata from the deployment and rotate secrets that may have been committed.' },
|
|
422
|
+
{ ruleId: 'exposed-metadata', sourceMarker: "kind: 'metadata'", title: 'Development metadata file is publicly readable', summary: 'A deployed metadata file is readable without authentication.', remediation: 'Remove the file from the deployment.' },
|
|
423
|
+
{ ruleId: 'exposed-backup', sourceMarker: "kind: 'backup'", title: 'Backup or database dump is publicly readable', summary: 'A deployed backup or database dump is readable without authentication.', remediation: 'Remove the file from the web root and review or rotate any exposed contents.' },
|
|
424
|
+
{ ruleId: 'exposed-credentials', sourceMarker: "kind: 'credentials'", title: 'Password file is publicly readable', summary: 'A deployed password file containing password hashes is readable without authentication.', remediation: 'Remove the file from the web root and change every password it lists.' },
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
rel: 'src/checks/live-basic/index.mjs',
|
|
429
|
+
definitions: [
|
|
430
|
+
['source-map', 'Browser source map is publicly readable', 'A publicly served source map can reveal readable source code and internal implementation details.', 'Do not deploy browser source maps publicly, or restrict them to authenticated developers.'],
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
rel: 'src/checks/live-js-secrets/index.mjs',
|
|
435
|
+
definitions: [
|
|
436
|
+
['supabase-service-role-jwt', 'Supabase service role key is served to visitors', 'A service role JWT in served JavaScript bypasses Row Level Security and grants broad project access.', 'Rotate the Supabase JWT secret or service key and use it only in server code.'],
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
rel: 'src/checks/dns-mail/index.mjs',
|
|
441
|
+
definitions: [
|
|
442
|
+
['spf-missing', 'Domain has no SPF record', 'Without SPF, receiving mail servers cannot verify which systems may send email for the domain.', 'Publish an SPF TXT record that names the approved mail provider.'],
|
|
443
|
+
['dmarc-missing', 'Domain has no DMARC record', 'Without DMARC, receiving systems have no policy for mail that fails SPF and DKIM.', 'Publish a DMARC record, then move from monitoring to quarantine or reject after reviewing reports.'],
|
|
444
|
+
['dmarc-none', 'DMARC policy is monitoring only', 'A DMARC p=none policy records failures but does not ask receivers to quarantine or reject spoofed mail.', 'After reviewing reports, move to quarantine and then reject.'],
|
|
445
|
+
['caa-missing', 'Domain has no CAA record', 'Without CAA, the domain does not publish a DNS restriction on which certificate authorities may issue certificates.', 'Publish CAA records naming the certificate authorities used by the domain.'],
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
rel: 'src/checks/posture/index.mjs',
|
|
450
|
+
definitions: [
|
|
451
|
+
['branch-protection', 'Default branch has no protection', 'Writers can push directly to the default branch or force-push without the reviewed controls checked by VSG.', 'Enable branch protection or a repository ruleset requiring pull requests and checks.'],
|
|
452
|
+
['secret_scanning', 'GitHub secret scanning is disabled', 'GitHub is not scanning this repository for exposed credentials.', 'Enable secret scanning in repository security settings.'],
|
|
453
|
+
['secret_scanning_push_protection', 'GitHub push protection is disabled', 'GitHub is not blocking commits that contain detected secrets.', 'Enable push protection in repository security settings.'],
|
|
454
|
+
['dependabot-alerts', 'Dependabot alerts are disabled', 'GitHub is not alerting on vulnerable dependencies in this repository.', 'Enable Dependabot alerts in repository security settings.'],
|
|
455
|
+
['actions-default-write', 'GitHub Actions default token has write access', 'Workflows without explicit permissions receive a repository token with write access.', 'Set the default workflow permission to read and grant write access only to jobs that need it.'],
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
rel: 'src/agent/schema.mjs',
|
|
460
|
+
producerRel: 'src/agent/ingest.mjs',
|
|
461
|
+
producerMarker: 'ruleId: clean(f.area, 60)',
|
|
462
|
+
definitions: (() => {
|
|
463
|
+
const declaredAreas = Object.keys(AGENT_REVIEW_AREA_METADATA);
|
|
464
|
+
if (AREAS.length !== declaredAreas.length || AREAS.some((area) => !Object.hasOwn(AGENT_REVIEW_AREA_METADATA, area))) {
|
|
465
|
+
fail('agent-review area metadata must exactly match the canonical schema areas.');
|
|
466
|
+
}
|
|
467
|
+
return AREAS.map((ruleId) => {
|
|
468
|
+
const { label, summary } = AGENT_REVIEW_AREA_METADATA[ruleId];
|
|
469
|
+
return {
|
|
470
|
+
ruleId,
|
|
471
|
+
sourceMarker: `'${ruleId}'`,
|
|
472
|
+
title: `Agent review: ${label}`,
|
|
473
|
+
summary,
|
|
474
|
+
remediation: `Review the finding, cited code and reproduction result locally. Validate the proposed change and tests for the ${label.toLowerCase()} boundary; the area label alone does not establish exploitability.`,
|
|
475
|
+
retest: `Run the VSG review again and verify no unresolved finding remains in the ${label.toLowerCase()} area and all required checks ran.`,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
})(),
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
for (const { rel, producerRel = null, producerMarker = null, definitions } of sources) {
|
|
482
|
+
if (!tracked.includes(rel)) fail(`built-in rule source ${rel} is missing.`);
|
|
483
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...rel.split('/')));
|
|
484
|
+
const source = bytes.toString('utf8');
|
|
485
|
+
let producerBytes = bytes;
|
|
486
|
+
if (producerRel) {
|
|
487
|
+
if (!tracked.includes(producerRel)) fail(`built-in rule producer source ${producerRel} is missing.`);
|
|
488
|
+
producerBytes = fs.readFileSync(path.join(sourceRoot, ...producerRel.split('/')));
|
|
489
|
+
if (!producerMarker || !producerBytes.toString('utf8').includes(producerMarker)) fail(`built-in rule producer ${producerRel} no longer matches its source metadata.`);
|
|
490
|
+
}
|
|
491
|
+
for (const item of definitions) {
|
|
492
|
+
const definition = Array.isArray(item)
|
|
493
|
+
? { ruleId: item[0], title: item[1], summary: item[2], remediation: item[3], retest: `Run the VSG check again after applying the repair.` }
|
|
494
|
+
: item;
|
|
495
|
+
const marker = definition.sourceMarker ?? definition.ruleId;
|
|
496
|
+
if (!source.includes(marker)) fail(`built-in rule ${definition.ruleId} no longer matches its tracked source metadata.`);
|
|
497
|
+
if (rules[definition.ruleId]) fail(`built-in rule ${definition.ruleId} is defined more than once.`);
|
|
498
|
+
const { sourceMarker: _sourceMarker, ...trusted } = definition;
|
|
499
|
+
rules[definition.ruleId] = { ...trusted, attributionIds: ['m2ai:vsg-original'] };
|
|
500
|
+
provenance[definition.ruleId] = producerRel
|
|
501
|
+
? { sourcePath: producerRel, sourceSha256: sha256(producerBytes), areaSourcePath: rel, areaSourceSha256: sha256(bytes), cweIds: [] }
|
|
502
|
+
: { sourcePath: rel, sourceSha256: sha256(bytes), cweIds: [] };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { rules, provenance };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function betterleaksRule(sourceRoot, tracked, contract) {
|
|
509
|
+
const required = [contract.evidencePath, ...BETTERLEAKS_NOTICE_PATHS];
|
|
510
|
+
const included = required.filter((rel) => tracked.includes(rel));
|
|
511
|
+
if (included.length === 0) {
|
|
512
|
+
return { rules: {}, credits: {}, provenance: {}, blockers: [contract.missingBlocker] };
|
|
513
|
+
}
|
|
514
|
+
if (included.length !== required.length) fail(`Betterleaks ${contract.ruleId} provenance is incomplete.`);
|
|
515
|
+
const evidenceBytes = fs.readFileSync(path.join(sourceRoot, ...contract.evidencePath.split('/')));
|
|
516
|
+
if (sha256(evidenceBytes) !== contract.evidenceSha256) fail(`Betterleaks ${contract.ruleId} provenance metadata was tampered with.`);
|
|
517
|
+
let evidence;
|
|
518
|
+
try { evidence = JSON.parse(evidenceBytes); } catch { fail(`Betterleaks ${contract.ruleId} provenance metadata is invalid JSON.`); }
|
|
519
|
+
const registryPath = 'tools/registry/betterleaks.json';
|
|
520
|
+
if (!tracked.includes(registryPath)) fail('the pinned Betterleaks registry record is missing.');
|
|
521
|
+
const registryBytes = fs.readFileSync(path.join(sourceRoot, ...registryPath.split('/')));
|
|
522
|
+
let registry;
|
|
523
|
+
try { registry = JSON.parse(registryBytes); } catch { fail('the pinned Betterleaks registry record is invalid JSON.'); }
|
|
524
|
+
const commercial = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'licensing', 'commercial-components.json'), 'utf8'));
|
|
525
|
+
const component = commercial.components?.find((item) => item.component === 'betterleaks');
|
|
526
|
+
const expected = evidence.component;
|
|
527
|
+
if (evidence.schemaVersion !== 1 || evidence.status !== 'engineering-provenance-record-not-legal-clearance'
|
|
528
|
+
|| expected?.name !== 'betterleaks' || expected.version !== '1.9.0' || expected.registryPath !== registryPath
|
|
529
|
+
|| expected.registrySha256 !== '502e6635125c26a2c895cbcf05b45f50424da771d559db01aa9125c9004a27eb'
|
|
530
|
+
|| evidence.rule?.id !== contract.ruleId || evidence.rule.source?.sha256 !== contract.sourceSha256
|
|
531
|
+
|| evidence.rule.generatedDefault?.sha256 !== 'a8f553eb634ac3c5c1ca3f0dc95e2b8abdea7d14b622604c5af07f30ce7926ef'
|
|
532
|
+
|| evidence.rule.defaultEmbedding?.sha256 !== '9e0b14688a364aa12519f1e0caa1f66ce03d365f8484fc720b9cb63f4ab68bae'
|
|
533
|
+
|| evidence.origin?.project !== 'gitleaks' || evidence.origin.sourceSha256 !== contract.originSourceSha256
|
|
534
|
+
|| evidence.origin.initialBetterleaksImportSha256 !== contract.initialImportSha256
|
|
535
|
+
|| !Array.isArray(evidence.licenses) || evidence.licenses.length !== 2) {
|
|
536
|
+
fail(`Betterleaks ${contract.ruleId} provenance metadata does not match the fixed candidate contract.`);
|
|
537
|
+
}
|
|
538
|
+
if (registry.version !== expected.version || sha256(registryBytes) !== expected.registrySha256
|
|
539
|
+
|| component?.version !== expected.version || component.registryPath !== registryPath
|
|
540
|
+
|| component.registrySha256 !== expected.registrySha256
|
|
541
|
+
|| component.sourceLicense?.sha256 !== 'caea114592a8f8e5e05a116d63a99e0ccd79a3ff74f4ddf270bcf4c929eb021e') {
|
|
542
|
+
fail('the Betterleaks registry, commercial record, and catalog provenance do not match.');
|
|
543
|
+
}
|
|
544
|
+
const expectedCredits = new Map([
|
|
545
|
+
[contract.attributionIds[0], {
|
|
546
|
+
path: BETTERLEAKS_NOTICE_PATHS[0],
|
|
547
|
+
sha256: 'caea114592a8f8e5e05a116d63a99e0ccd79a3ff74f4ddf270bcf4c929eb021e',
|
|
548
|
+
}],
|
|
549
|
+
[contract.attributionIds[1], {
|
|
550
|
+
path: BETTERLEAKS_NOTICE_PATHS[1],
|
|
551
|
+
sha256: 'e3884b252b3bfc045e55be43a34d1e80da070bc6f804ac95bf4660e97d62ebc6',
|
|
552
|
+
}],
|
|
553
|
+
]);
|
|
554
|
+
const credits = {};
|
|
555
|
+
const notices = [];
|
|
556
|
+
for (const license of evidence.licenses) {
|
|
557
|
+
const fixed = expectedCredits.get(license.attributionId);
|
|
558
|
+
if (!fixed || license.spdxExpression !== 'MIT' || license.noticePath !== fixed.path
|
|
559
|
+
|| license.noticeSha256 !== fixed.sha256 || license.noticeBytes !== 1069
|
|
560
|
+
|| typeof license.displayName !== 'string' || !license.displayName
|
|
561
|
+
|| !/^https:\/\//.test(license.sourceUrl ?? '') || !/^https:\/\//.test(license.licenseUrl ?? '')
|
|
562
|
+
|| typeof license.modificationNotice !== 'string' || !license.modificationNotice) {
|
|
563
|
+
fail(`Betterleaks ${contract.ruleId} credit metadata does not match the fixed notice contract.`);
|
|
564
|
+
}
|
|
565
|
+
const noticeBytes = fs.readFileSync(path.join(sourceRoot, ...fixed.path.split('/')));
|
|
566
|
+
if (noticeBytes.byteLength !== license.noticeBytes || sha256(noticeBytes) !== fixed.sha256) {
|
|
567
|
+
fail(`packaged notice ${fixed.path} does not match its verified upstream bytes.`);
|
|
568
|
+
}
|
|
569
|
+
credits[license.attributionId] = {
|
|
570
|
+
attributionId: license.attributionId,
|
|
571
|
+
displayName: license.displayName,
|
|
572
|
+
sourceUrl: license.sourceUrl,
|
|
573
|
+
licenseName: 'MIT',
|
|
574
|
+
licenseUrl: license.licenseUrl,
|
|
575
|
+
modificationNotice: license.modificationNotice,
|
|
576
|
+
};
|
|
577
|
+
notices.push({ path: fixed.path, sha256: fixed.sha256, bytes: license.noticeBytes });
|
|
578
|
+
}
|
|
579
|
+
if (Object.keys(credits).length !== 2) fail(`Betterleaks ${contract.ruleId} credits are incomplete.`);
|
|
580
|
+
const ruleId = evidence.rule.id;
|
|
581
|
+
return {
|
|
582
|
+
rules: {
|
|
583
|
+
[ruleId]: {
|
|
584
|
+
ruleId,
|
|
585
|
+
title: evidence.rule.displayName,
|
|
586
|
+
summary: contract.summary,
|
|
587
|
+
remediation: contract.remediation,
|
|
588
|
+
retest: contract.retest,
|
|
589
|
+
attributionIds: [...expectedCredits.keys()],
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
credits,
|
|
593
|
+
provenance: {
|
|
594
|
+
[ruleId]: {
|
|
595
|
+
sourcePath: contract.evidencePath,
|
|
596
|
+
sourceSha256: contract.evidenceSha256,
|
|
597
|
+
component: { name: expected.name, version: expected.version, registryPath, registrySha256: expected.registrySha256 },
|
|
598
|
+
upstreamSources: [evidence.rule.source, ...(evidence.rule.filterSource ? [evidence.rule.filterSource] : []), evidence.rule.generatedDefault, evidence.rule.defaultEmbedding, {
|
|
599
|
+
path: evidence.origin.sourcePath, url: evidence.origin.sourceUrl, sha256: evidence.origin.sourceSha256,
|
|
600
|
+
}],
|
|
601
|
+
notices,
|
|
602
|
+
cweIds: [],
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
blockers: [],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function qualificationEvidence(sourceRoot, tracked) {
|
|
610
|
+
const present = QUALIFICATION_RECORDS.filter((item) => tracked.includes(item.path));
|
|
611
|
+
if (present.length === 0) return null; // Legacy artifacts contain neither record.
|
|
612
|
+
if (present.length !== QUALIFICATION_RECORDS.length) fail('qualification evidence records must be packaged together.');
|
|
613
|
+
const parsed = [];
|
|
614
|
+
const records = present.map((item) => {
|
|
615
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...item.path.split('/')));
|
|
616
|
+
if (sha256(bytes) !== item.sha256) fail(`qualification evidence ${item.path} does not match its reviewed bytes.`);
|
|
617
|
+
parsed.push(JSON.parse(bytes));
|
|
618
|
+
return { ...item, bytes: bytes.byteLength };
|
|
619
|
+
});
|
|
620
|
+
const [candidate, notices] = parsed;
|
|
621
|
+
if (canonical(candidate.releaseEligibility) !== canonical({ unchanged: true, status: 'blocked', blockers: RELEASE_BLOCKERS })
|
|
622
|
+
|| candidate.yara.sourceQualification.runtimePackaged !== false
|
|
623
|
+
|| candidate.yara.sourceQualification.commerciallyCleared !== false
|
|
624
|
+
|| notices.installed !== false || notices.commerciallyApproved !== false) {
|
|
625
|
+
fail('qualification evidence cannot grant runtime packaging or commercial approval.');
|
|
626
|
+
}
|
|
627
|
+
const selectedRuleRuntimeImplemented = Object.values(SELECTED_RUNTIME_RECORDS).every((record) => tracked.includes(record.path));
|
|
628
|
+
return {
|
|
629
|
+
status: 'packaged-engineering-evidence-only', records,
|
|
630
|
+
sastSelectedFiles: candidate.sast.selectedRuleCount,
|
|
631
|
+
yaraSelectedRules: candidate.yara.conditionalCandidateCount,
|
|
632
|
+
yaraExcludedHolds: candidate.yara.holdCount,
|
|
633
|
+
yaraStrictCompile: candidate.yara.sourceQualification.strictCompile,
|
|
634
|
+
selectedSourcesDelivered: false, selectedRuleRuntimePackaged: false,
|
|
635
|
+
selectedRuleRuntimeImplemented,
|
|
636
|
+
selectedSourceAcquisition: selectedRuleRuntimeImplemented ? 'fixed-upstream-files-and-archive-in-disposable-linux-volumes' : null,
|
|
637
|
+
boundedCoveragePassed: selectedRuleRuntimeImplemented,
|
|
638
|
+
coverageEquivalenceProven: false, commerciallyCleared: false, releaseApproval: null,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function commercialApprovalMaterial(sourceRoot, artifactFiles) {
|
|
643
|
+
const files = artifactFiles.filter((rel) => rel !== COMMERCIAL_RELEASE_RECORD.path
|
|
644
|
+
&& rel !== EVALUATOR_PATH && rel !== 'lattice-catalog.json').sort();
|
|
645
|
+
const hash = crypto.createHash('sha256');
|
|
646
|
+
for (const rel of files) {
|
|
647
|
+
hash.update(rel);
|
|
648
|
+
hash.update('\0');
|
|
649
|
+
hash.update(fs.readFileSync(path.join(sourceRoot, ...rel.split('/'))));
|
|
650
|
+
hash.update('\0');
|
|
651
|
+
}
|
|
652
|
+
return { algorithm: 'sha256-path-nul-bytes-nul', sha256: hash.digest('hex'), files: files.length };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function commercialReleaseEvidence(sourceRoot, tracked, artifactFiles) {
|
|
656
|
+
if (!tracked.includes(COMMERCIAL_RELEASE_RECORD.path)) return null;
|
|
657
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...COMMERCIAL_RELEASE_RECORD.path.split('/')));
|
|
658
|
+
let record;
|
|
659
|
+
try { record = JSON.parse(bytes); } catch { fail('commercial runtime release record is invalid JSON.'); }
|
|
660
|
+
const approvedSource = commercialApprovalMaterial(sourceRoot, artifactFiles);
|
|
661
|
+
const evidence = COMMERCIAL_RELEASE_EVIDENCE.map((expected) => {
|
|
662
|
+
if (!tracked.includes(expected.path)) fail(`commercial release evidence ${expected.path} is missing.`);
|
|
663
|
+
const evidenceBytes = fs.readFileSync(path.join(sourceRoot, ...expected.path.split('/')));
|
|
664
|
+
if (sha256(evidenceBytes) !== expected.sha256) fail(`commercial release evidence ${expected.path} changed from its reviewed bytes.`);
|
|
665
|
+
return { ...expected, bytes: evidenceBytes.byteLength };
|
|
666
|
+
});
|
|
667
|
+
const expectedEvidence = COMMERCIAL_RELEASE_EVIDENCE.map(({ path: evidencePath, sha256: digest }) => ({ path: evidencePath, sha256: digest }));
|
|
668
|
+
if (record.schemaVersion !== 1 || record.status !== 'owner-approved-commercial-runtime-release'
|
|
669
|
+
|| record.owner !== 'M2 AI, LLC' || record.formationJurisdiction !== 'Texas'
|
|
670
|
+
|| record.reviewProfile?.legalAdviceClaimed !== false
|
|
671
|
+
|| record.authorization?.publicDomainAssumptions !== false
|
|
672
|
+
|| record.authorization?.checkoutApproved !== false
|
|
673
|
+
|| record.authorization?.paidPublicationApproved !== false
|
|
674
|
+
|| record.authorization?.commercialRuntimeExecutionApproved !== true
|
|
675
|
+
|| record.authorization?.protectedHostedValidationApproved !== true
|
|
676
|
+
|| canonical(record.approvedSource) !== canonical(approvedSource)
|
|
677
|
+
|| record.runtimeBoundary?.rulePayloadDistributionByM2 !== false
|
|
678
|
+
|| record.runtimeBoundary?.fallback?.restrictedSemgrepRules !== false
|
|
679
|
+
|| record.runtimeBoundary?.fallback?.mixedYaraForgePack !== false
|
|
680
|
+
|| record.runtimeBoundary?.fallback?.callerYaraRules !== false
|
|
681
|
+
|| record.selectedSources?.sast?.ruleFiles !== 161
|
|
682
|
+
|| record.selectedSources?.yara?.selectedRules !== 4536
|
|
683
|
+
|| record.selectedSources?.yara?.heldRules !== 574
|
|
684
|
+
|| record.selectedSources?.yara?.selectionDigest !== '33aa825b1f9c843e7129f26b5b57e85d6121562cdeb577e40aef9360601585f6'
|
|
685
|
+
|| canonical(record.evidence) !== canonical(expectedEvidence)
|
|
686
|
+
|| record.coverageDecision?.sastBoundedCoveragePassed !== true
|
|
687
|
+
|| record.coverageDecision?.yaraSelectedPackExecutionPassed !== true
|
|
688
|
+
|| record.coverageDecision?.cleanControlsPassed !== true
|
|
689
|
+
|| record.coverageDecision?.coverageEquivalenceClaimed !== false
|
|
690
|
+
|| record.coverageDecision?.approvedForCommercialRuntime !== true
|
|
691
|
+
|| record.releaseEligibility?.status !== 'approved'
|
|
692
|
+
|| canonical(record.releaseEligibility?.closedGates) !== canonical(RELEASE_BLOCKERS)
|
|
693
|
+
|| record.releaseEligibility?.checkoutEnabled !== false
|
|
694
|
+
|| record.releaseEligibility?.paidPublicationEnabled !== false) {
|
|
695
|
+
fail('commercial runtime release record exceeds or does not meet the fixed owner-approved boundary.');
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
record,
|
|
699
|
+
ref: { path: COMMERCIAL_RELEASE_RECORD.path, sha256: sha256(bytes), bytes: bytes.byteLength },
|
|
700
|
+
approvedSource,
|
|
701
|
+
evidence,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function fixedJsonRecord(sourceRoot, tracked, record, label) {
|
|
706
|
+
if (!tracked.includes(record.path)) fail(`${label} record ${record.path} is missing.`);
|
|
707
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...record.path.split('/')));
|
|
708
|
+
if (sha256(bytes) !== record.sha256) fail(`${label} record ${record.path} changed from its reviewed bytes.`);
|
|
709
|
+
let value;
|
|
710
|
+
try { value = JSON.parse(bytes); } catch { fail(`${label} record ${record.path} is invalid JSON.`); }
|
|
711
|
+
return { value, ref: { ...record, bytes: bytes.byteLength } };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function packagedNotice(sourceRoot, tracked, ref, label) {
|
|
715
|
+
if (!ref || !SHA256.test(ref.sha256 ?? '') || ref.path !== `licensing/notices/candidate-sources/sha256/${ref.sha256}.txt`
|
|
716
|
+
|| !Number.isSafeInteger(ref.bytes) || ref.bytes <= 0 || !tracked.includes(ref.path)) {
|
|
717
|
+
fail(`${label} notice reference is incomplete or is not packaged.`);
|
|
718
|
+
}
|
|
719
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...ref.path.split('/')));
|
|
720
|
+
if (bytes.byteLength !== ref.bytes || sha256(bytes) !== ref.sha256) fail(`${label} notice bytes do not match the reviewed record.`);
|
|
721
|
+
return { path: ref.path, sha256: ref.sha256, bytes: ref.bytes };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function sastRuleId(sourcePath) {
|
|
725
|
+
const parts = sourcePath.replace(/\.ya?ml$/i, '').split('/');
|
|
726
|
+
let file = parts.pop().replaceAll('_', '-');
|
|
727
|
+
const prefix = parts.map((part) => part.replaceAll('_', '-')).join('_');
|
|
728
|
+
// These two upstream Python rules use a dotted namespace in the declared rule id.
|
|
729
|
+
if (prefix === 'python_crypto' && /^rule-crypto-hazmat-hash-(?:md5|sha1)$/.test(file)) {
|
|
730
|
+
file = file.replace('rule-crypto-hazmat', 'rule-crypto.hazmat');
|
|
731
|
+
}
|
|
732
|
+
return `${prefix}_${file}`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function selectedRuntimeCatalog(sourceRoot, tracked) {
|
|
736
|
+
const present = Object.values(SELECTED_RUNTIME_RECORDS).filter((record) => tracked.includes(record.path));
|
|
737
|
+
if (present.length === 0) return null; // Legacy/local fixture artifacts do not opt into the selected runtime.
|
|
738
|
+
if (present.length !== Object.keys(SELECTED_RUNTIME_RECORDS).length) {
|
|
739
|
+
fail('selected runtime records must be packaged together.');
|
|
740
|
+
}
|
|
741
|
+
const parsed = {};
|
|
742
|
+
const refs = {};
|
|
743
|
+
for (const [name, record] of Object.entries(SELECTED_RUNTIME_RECORDS)) {
|
|
744
|
+
if (!tracked.includes(record.path)) fail(`selected runtime record ${record.path} is missing.`);
|
|
745
|
+
const bytes = fs.readFileSync(path.join(sourceRoot, ...record.path.split('/')));
|
|
746
|
+
if (sha256(bytes) !== record.sha256) fail(`selected runtime record ${record.path} changed from its reviewed bytes.`);
|
|
747
|
+
parsed[name] = JSON.parse(bytes);
|
|
748
|
+
refs[name] = { ...record, bytes: bytes.byteLength };
|
|
749
|
+
}
|
|
750
|
+
const { sastSourceEvidence: sast, sastIdentities: identities, yaraInventory: inventory, coverage } = parsed;
|
|
751
|
+
if (sast.sourceCommit !== '53bf5cf6df3c51b6c02110f5a638b5e6213666cd'
|
|
752
|
+
|| sast.sourceArchiveSha256 !== '32b51522401d1a19e2f68277235eef1f31ddd3560ae54343b6a6a78c4d2fbb23'
|
|
753
|
+
|| sast.selectedRules !== 161 || !Array.isArray(sast.files) || sast.files.length !== 163
|
|
754
|
+
|| identities.ruleCount !== 161 || identities.ruleIdsSha256 !== '338143b7ac9e7bfd90f432b7aec070aa710584eaecfeb6174339a8852e1ae611'
|
|
755
|
+
|| !Array.isArray(identities.ruleIds) || identities.ruleIds.length !== 161
|
|
756
|
+
|| sha256(JSON.stringify(identities.ruleIds)) !== identities.ruleIdsSha256) {
|
|
757
|
+
fail('selected SAST runtime records do not match the fixed 161-rule contract.');
|
|
758
|
+
}
|
|
759
|
+
const candidateDecisions = new Set(['candidate-attribution', 'candidate-attribution-sharealike', 'candidate-explicit-apache']);
|
|
760
|
+
const normalized = (rule) => ({
|
|
761
|
+
rule: rule.rule, blockSha256: rule.blockSha256, author: rule.author, sourceUrl: rule.sourceUrl,
|
|
762
|
+
candidateLicense: rule.candidateLicense, decision: rule.decision, licenseMetadata: rule.licenseMetadata,
|
|
763
|
+
});
|
|
764
|
+
const selectedYara = inventory.rules.filter((rule) => candidateDecisions.has(rule.decision)).map(normalized).sort((a, b) => a.rule.localeCompare(b.rule));
|
|
765
|
+
const heldYara = inventory.rules.filter((rule) => !candidateDecisions.has(rule.decision)).map(normalized).sort((a, b) => a.rule.localeCompare(b.rule));
|
|
766
|
+
const selectionDigest = sha256(canonical(selectedYara));
|
|
767
|
+
const holdDigest = sha256(canonical(heldYara));
|
|
768
|
+
if (inventory.archiveVersion !== '20260927' || inventory.archiveSha256 !== '16180800cc2f077638d2e13a33bd68609d0b6eedd1fbfdbae6d5d4a1c53f826f'
|
|
769
|
+
|| selectedYara.length !== 4536 || heldYara.length !== 574
|
|
770
|
+
|| selectionDigest !== '33aa825b1f9c843e7129f26b5b57e85d6121562cdeb577e40aef9360601585f6'
|
|
771
|
+
|| holdDigest !== '22150df654bf4396c085336f1c00f8d0483cdc61713a81db32a115c586ef98e2') {
|
|
772
|
+
fail('selected YARA runtime records do not match the fixed 4,536-rule contract.');
|
|
773
|
+
}
|
|
774
|
+
if (coverage.status !== 'real-selected-runtime-coverage-evidence-not-release-clearance'
|
|
775
|
+
|| coverage.sast?.boundedCoveragePassed !== true || coverage.yara?.boundedCoveragePassed !== true
|
|
776
|
+
|| coverage.coverageDecision?.status !== 'approved-for-protected-test-qualification'
|
|
777
|
+
|| coverage.coverageDecision?.publicCommercialReleaseApproved !== false || coverage.coverageDecision?.checkoutApproved !== false) {
|
|
778
|
+
fail('selected runtime coverage record exceeds or does not meet the protected-test qualification boundary.');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const rules = {};
|
|
782
|
+
const provenance = {};
|
|
783
|
+
const licenseFiles = new Map(sast.files.filter((item) => item.path === 'LICENSE' || item.path === 'rules/lgpl/LICENSE').map((item) => [item.path, item]));
|
|
784
|
+
const sastFilesByRule = new Map();
|
|
785
|
+
for (const file of sast.files.filter((item) => /\.ya?ml$/i.test(item.path ?? ''))) {
|
|
786
|
+
const ruleId = sastRuleId(file.path);
|
|
787
|
+
if (sastFilesByRule.has(ruleId)) fail(`selected SAST source identity ${ruleId} is duplicated.`);
|
|
788
|
+
const licenseFile = licenseFiles.get(file.licensePath);
|
|
789
|
+
if (!licenseFile || file.licenseSha256 !== licenseFile.sha256 || file.modified !== false || file.headerPreserved !== true
|
|
790
|
+
|| !SHA256.test(file.sha256 ?? '') || !/^https:\/\//.test(file.sourceUrl ?? '')) {
|
|
791
|
+
fail(`selected SAST source evidence for ${ruleId} is incomplete.`);
|
|
792
|
+
}
|
|
793
|
+
sastFilesByRule.set(ruleId, { ...file, licenseFile });
|
|
794
|
+
}
|
|
795
|
+
if (sastFilesByRule.size !== 161 || identities.ruleIds.some((ruleId) => !sastFilesByRule.has(ruleId))) {
|
|
796
|
+
fail('selected SAST source files do not map exactly to the 161 selected rule identities.');
|
|
797
|
+
}
|
|
798
|
+
const sastNotice = {};
|
|
799
|
+
for (const [licensePath, file] of licenseFiles) {
|
|
800
|
+
sastNotice[licensePath] = packagedNotice(sourceRoot, tracked, {
|
|
801
|
+
path: `licensing/notices/candidate-sources/sha256/${file.sha256}.txt`,
|
|
802
|
+
sha256: file.sha256,
|
|
803
|
+
bytes: file.bytes,
|
|
804
|
+
}, `selected SAST ${file.declaredLicense}`);
|
|
805
|
+
}
|
|
806
|
+
const credits = {
|
|
807
|
+
'gitlab-sast:mit': {
|
|
808
|
+
attributionId: 'gitlab-sast:mit', displayName: 'GitLab SAST Rules selected MIT sources',
|
|
809
|
+
sourceUrl: `https://gitlab.com/gitlab-org/security-products/sast-rules/-/tree/${sast.sourceCommit}`,
|
|
810
|
+
licenseName: 'MIT', licenseUrl: 'https://opensource.org/license/mit',
|
|
811
|
+
modificationNotice: 'Selected source files are used byte-for-byte from the pinned source commit.',
|
|
812
|
+
notices: [sastNotice.LICENSE],
|
|
813
|
+
},
|
|
814
|
+
'gitlab-sast:lgpl-3.0': {
|
|
815
|
+
attributionId: 'gitlab-sast:lgpl-3.0', displayName: 'GitLab SAST Rules selected LGPL-3.0 sources',
|
|
816
|
+
sourceUrl: `https://gitlab.com/gitlab-org/security-products/sast-rules/-/tree/${sast.sourceCommit}/rules/lgpl/javascript`,
|
|
817
|
+
licenseName: 'LGPL-3.0', licenseUrl: 'https://www.gnu.org/licenses/lgpl-3.0.html',
|
|
818
|
+
modificationNotice: 'Selected source files are used byte-for-byte from the pinned source commit and remain separately replaceable.',
|
|
819
|
+
notices: [sastNotice['rules/lgpl/LICENSE']],
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
for (const ruleId of identities.ruleIds) {
|
|
823
|
+
const sourceFile = sastFilesByRule.get(ruleId);
|
|
824
|
+
const attributionId = sourceFile.declaredLicense === 'LGPL-3.0' ? 'gitlab-sast:lgpl-3.0'
|
|
825
|
+
: sourceFile.declaredLicense === 'MIT' ? 'gitlab-sast:mit' : null;
|
|
826
|
+
if (!attributionId) fail(`selected SAST source ${sourceFile.path} has an unsupported declared license.`);
|
|
827
|
+
rules[ruleId] = {
|
|
828
|
+
ruleId,
|
|
829
|
+
title: `SAST detection: ${ruleId}`,
|
|
830
|
+
summary: 'The selected static-analysis rule found a security-sensitive code pattern.',
|
|
831
|
+
remediation: 'Review the affected code path, remove the unsafe pattern or add the required validation and authorization, then rerun the same Lattice Security check.',
|
|
832
|
+
retest: `Run the selected SAST rule ${ruleId} again after applying the repair.`,
|
|
833
|
+
attributionIds: [attributionId],
|
|
834
|
+
};
|
|
835
|
+
provenance[ruleId] = {
|
|
836
|
+
sourcePath: sourceFile.path,
|
|
837
|
+
sourceSha256: sourceFile.sha256,
|
|
838
|
+
sourceUrl: sourceFile.sourceUrl,
|
|
839
|
+
sourceCommit: sast.sourceCommit,
|
|
840
|
+
sourceEvidence: refs.sastSourceEvidence,
|
|
841
|
+
declaredLicense: sourceFile.declaredLicense,
|
|
842
|
+
licenseSourcePath: sourceFile.licensePath,
|
|
843
|
+
notices: [sastNotice[sourceFile.licensePath]],
|
|
844
|
+
cweIds: [],
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const noticeRecords = {};
|
|
849
|
+
const noticeRefs = {};
|
|
850
|
+
for (const [name, record] of Object.entries(SELECTED_NOTICE_RECORDS)) {
|
|
851
|
+
const loaded = fixedJsonRecord(sourceRoot, tracked, record, `selected ${name}`);
|
|
852
|
+
noticeRecords[name] = loaded.value;
|
|
853
|
+
noticeRefs[name] = loaded.ref;
|
|
854
|
+
}
|
|
855
|
+
const sourceLicenses = new Map();
|
|
856
|
+
for (const license of inventory.sourceLicenseTexts ?? []) {
|
|
857
|
+
const notice = packagedNotice(sourceRoot, tracked, {
|
|
858
|
+
path: `licensing/notices/candidate-sources/sha256/${license.textSha256}.txt`,
|
|
859
|
+
sha256: license.textSha256,
|
|
860
|
+
bytes: fs.statSync(path.join(sourceRoot, 'licensing', 'notices', 'candidate-sources', 'sha256', `${license.textSha256}.txt`)).size,
|
|
861
|
+
}, `selected YARA ${license.metadataUrl}`);
|
|
862
|
+
if (sourceLicenses.has(license.metadataUrl)) fail(`selected YARA license source ${license.metadataUrl} is duplicated.`);
|
|
863
|
+
sourceLicenses.set(license.metadataUrl, { ...license, notice });
|
|
864
|
+
}
|
|
865
|
+
const malpedia = noticeRecords.malpedia;
|
|
866
|
+
const malpediaNotices = [
|
|
867
|
+
packagedNotice(sourceRoot, tracked, malpedia.declaration, 'Malpedia source declaration'),
|
|
868
|
+
packagedNotice(sourceRoot, tracked, malpedia.legalcode, 'Malpedia legalcode'),
|
|
869
|
+
];
|
|
870
|
+
if (malpedia.status !== 'source-license-declaration-evidence-not-rights-clearance' || malpedia.commerciallyCleared !== false
|
|
871
|
+
|| malpedia.selection?.rules !== 1582 || malpedia.selection?.digest !== '1abee35347277118e5a744e8837bb6ac88147196ef80bd527dc833f6b840b042') {
|
|
872
|
+
fail('Malpedia selected-source declaration evidence does not match the fixed non-clearance record.');
|
|
873
|
+
}
|
|
874
|
+
const apache = noticeRecords.apache;
|
|
875
|
+
const apacheNotice = packagedNotice(sourceRoot, tracked, apache.legalcode, 'selected original-author Apache legalcode');
|
|
876
|
+
if (apache.status !== 'source-license-declaration-evidence-not-rights-clearance' || apache.commerciallyCleared !== false
|
|
877
|
+
|| apache.selection?.rules !== 27 || apache.selection?.sha256 !== '7049543e6494f9dc40b0591b975c53d03f8823522d3679f81e04a457c32d1108'
|
|
878
|
+
|| !Array.isArray(apache.declarations) || apache.declarations.length !== 27 || apache.unmappedRules?.length !== 0) {
|
|
879
|
+
fail('selected original-author Apache declaration evidence does not match the fixed non-clearance record.');
|
|
880
|
+
}
|
|
881
|
+
const apacheDeclarations = new Map(apache.declarations.map((item) => [item.rule, item]));
|
|
882
|
+
for (const ref of apache.originalEvidenceRecords ?? []) {
|
|
883
|
+
const fixed = Object.values(SELECTED_NOTICE_RECORDS).find((item) => item.path === ref.path);
|
|
884
|
+
if (!fixed || fixed.sha256 !== ref.sha256 || noticeRefs[Object.keys(SELECTED_NOTICE_RECORDS).find((key) => SELECTED_NOTICE_RECORDS[key].path === ref.path)]?.bytes !== ref.bytes) {
|
|
885
|
+
fail(`selected Apache original evidence ${ref.path} does not match its packaged record.`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
for (const rule of selectedYara) {
|
|
889
|
+
let attributionId;
|
|
890
|
+
let licenseEvidence;
|
|
891
|
+
if (rule.decision === 'candidate-attribution') {
|
|
892
|
+
const sourceLicense = sourceLicenses.get(rule.licenseMetadata?.license_url);
|
|
893
|
+
if (!sourceLicense || sourceLicense.observedLicense !== rule.candidateLicense) {
|
|
894
|
+
fail(`selected YARA rule ${rule.rule} lacks its origin-specific license text.`);
|
|
895
|
+
}
|
|
896
|
+
attributionId = `yara-source:${sha256(sourceLicense.metadataUrl).slice(0, 16)}`;
|
|
897
|
+
if (!credits[attributionId]) credits[attributionId] = {
|
|
898
|
+
attributionId,
|
|
899
|
+
displayName: `Selected YARA rules from ${new URL(sourceLicense.metadataUrl).hostname}`,
|
|
900
|
+
sourceUrl: sourceLicense.metadataUrl,
|
|
901
|
+
licenseName: sourceLicense.observedLicense,
|
|
902
|
+
licenseUrl: sourceLicense.metadataUrl,
|
|
903
|
+
modificationNotice: 'Lattice executes a frozen selection from the pinned aggregate. The original source and exact observed license text remain identified; aggregate adaptation status is not asserted.',
|
|
904
|
+
notices: [sourceLicense.notice],
|
|
905
|
+
};
|
|
906
|
+
licenseEvidence = { kind: 'origin-license-text', sourceUrl: sourceLicense.metadataUrl, notices: [sourceLicense.notice] };
|
|
907
|
+
} else if (rule.decision === 'candidate-attribution-sharealike') {
|
|
908
|
+
attributionId = 'yara-source:malpedia-cc-by-sa-4.0';
|
|
909
|
+
if (!credits[attributionId]) credits[attributionId] = {
|
|
910
|
+
attributionId,
|
|
911
|
+
displayName: 'Malpedia signator rules selected sources',
|
|
912
|
+
sourceUrl: `${malpedia.sourceRepository}/tree/${malpedia.sourceCommit}/rules`,
|
|
913
|
+
licenseName: 'CC-BY-SA-4.0',
|
|
914
|
+
licenseUrl: malpedia.legalcode.sourceUrl,
|
|
915
|
+
modificationNotice: 'The aggregate block is source-identified, but equality to the original and adaptation status are not established. This record does not assert completed attribution, change-notice, source-distribution, or ShareAlike compliance.',
|
|
916
|
+
notices: malpediaNotices,
|
|
917
|
+
evidenceRecords: [noticeRefs.malpedia],
|
|
918
|
+
};
|
|
919
|
+
licenseEvidence = { kind: 'rules-folder-declaration', evidenceRecord: noticeRefs.malpedia, notices: malpediaNotices };
|
|
920
|
+
} else if (rule.decision === 'candidate-explicit-apache') {
|
|
921
|
+
const declaration = apacheDeclarations.get(rule.rule);
|
|
922
|
+
if (!declaration || declaration.blockSha256 !== rule.blockSha256 || declaration.aggregatorSourceUrl !== rule.sourceUrl
|
|
923
|
+
|| declaration.declaredLicense !== 'Apache-2.0' || !/^https:\/\//.test(declaration.originalSourceUrl ?? '')
|
|
924
|
+
|| (declaration.sourceUrl !== undefined && !/^https:\/\//.test(declaration.sourceUrl))) {
|
|
925
|
+
fail(`selected YARA rule ${rule.rule} lacks an exact original-author Apache declaration.`);
|
|
926
|
+
}
|
|
927
|
+
const evidenceRecord = Object.values(noticeRefs).find((ref) => ref.path === declaration.evidenceRecord);
|
|
928
|
+
if (!evidenceRecord) fail(`selected YARA rule ${rule.rule} references an unpackaged original-author evidence record.`);
|
|
929
|
+
attributionId = `yara-apache:${sha256(rule.rule).slice(0, 16)}`;
|
|
930
|
+
credits[attributionId] = {
|
|
931
|
+
attributionId,
|
|
932
|
+
displayName: `${rule.author} selected YARA source`,
|
|
933
|
+
sourceUrl: declaration.originalSourceUrl,
|
|
934
|
+
licenseName: 'Apache-2.0',
|
|
935
|
+
licenseUrl: apache.legalcode.sourceUrl,
|
|
936
|
+
modificationNotice: 'The original-author page declares Apache-2.0. Equality to the aggregate block, adaptation status, and completed distribution obligations are not asserted.',
|
|
937
|
+
notices: [apacheNotice],
|
|
938
|
+
evidenceRecords: [evidenceRecord],
|
|
939
|
+
};
|
|
940
|
+
licenseEvidence = {
|
|
941
|
+
kind: declaration.sourceKind,
|
|
942
|
+
originalSourceUrl: declaration.originalSourceUrl,
|
|
943
|
+
...(declaration.sourceUrl ? { declarationSourceUrl: declaration.sourceUrl } : {}),
|
|
944
|
+
declarationSha256: declaration.declarationSha256,
|
|
945
|
+
evidenceRecord,
|
|
946
|
+
notices: [apacheNotice],
|
|
947
|
+
};
|
|
948
|
+
} else {
|
|
949
|
+
fail(`selected YARA rule ${rule.rule} has an unsupported selection decision.`);
|
|
950
|
+
}
|
|
951
|
+
rules[rule.rule] = {
|
|
952
|
+
ruleId: rule.rule,
|
|
953
|
+
title: `YARA detection: ${rule.rule}`,
|
|
954
|
+
summary: `A selected YARA rule by ${rule.author} matched the scanned file.`,
|
|
955
|
+
remediation: 'Do not ship the matched file until it has been removed or rebuilt from a trusted source and the same selected YARA check no longer matches.',
|
|
956
|
+
retest: `Run selected YARA rule ${rule.rule} again after replacing or removing the file.`,
|
|
957
|
+
attributionIds: [attributionId],
|
|
958
|
+
author: rule.author,
|
|
959
|
+
sourceUrl: rule.sourceUrl,
|
|
960
|
+
licenseName: rule.candidateLicense,
|
|
961
|
+
};
|
|
962
|
+
provenance[rule.rule] = {
|
|
963
|
+
sourcePath: refs.yaraInventory.path,
|
|
964
|
+
sourceSha256: refs.yaraInventory.sha256,
|
|
965
|
+
sourceUrl: rule.sourceUrl,
|
|
966
|
+
sourceBlockSha256: rule.blockSha256,
|
|
967
|
+
author: rule.author,
|
|
968
|
+
licenseName: rule.candidateLicense,
|
|
969
|
+
decision: rule.decision,
|
|
970
|
+
licenseEvidence,
|
|
971
|
+
cweIds: [],
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return {
|
|
976
|
+
contract: {
|
|
977
|
+
schemaVersion: 2,
|
|
978
|
+
fallback: { semgrepRules: 'forbidden', yaraForgeMixedPack: 'forbidden', yaraExtraRules: 'forbidden' },
|
|
979
|
+
coverage: refs.coverage,
|
|
980
|
+
notices: Object.values(noticeRefs),
|
|
981
|
+
sast: {
|
|
982
|
+
source: {
|
|
983
|
+
mode: 'files',
|
|
984
|
+
sourceCommit: sast.sourceCommit,
|
|
985
|
+
files: sast.files.length,
|
|
986
|
+
totalBytes: sast.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
987
|
+
manifestSha256: sha256(JSON.stringify(sast.files.map((file) => ({ path: file.path, url: file.sourceUrl, sha256: file.sha256, bytes: file.bytes })))),
|
|
988
|
+
maxBytes: 1024 * 1024,
|
|
989
|
+
},
|
|
990
|
+
sourceEvidence: refs.sastSourceEvidence,
|
|
991
|
+
identities: refs.sastIdentities,
|
|
992
|
+
},
|
|
993
|
+
yara: {
|
|
994
|
+
source: {
|
|
995
|
+
mode: 'archive',
|
|
996
|
+
url: 'https://github.com/YARAHQ/yara-forge/releases/download/20260927/yara-forge-rules-core.zip',
|
|
997
|
+
sha256: inventory.archiveSha256,
|
|
998
|
+
maxBytes: 16 * 1024 * 1024,
|
|
999
|
+
},
|
|
1000
|
+
inventory: refs.yaraInventory,
|
|
1001
|
+
selectionDigest,
|
|
1002
|
+
holdDigest,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
rules,
|
|
1006
|
+
credits,
|
|
1007
|
+
provenance,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function runtimeCatalog(sourceRoot, tracked, sourceCommit, release, originalSourceArchiveUrl) {
|
|
1012
|
+
const original = originalRules(sourceRoot, tracked);
|
|
1013
|
+
const builtins = builtinRules(sourceRoot, tracked);
|
|
1014
|
+
const betterleaks = BETTERLEAKS_RULE_CONTRACTS.map((contract) => betterleaksRule(sourceRoot, tracked, contract));
|
|
1015
|
+
const betterleaksRules = Object.assign({}, ...betterleaks.map((item) => item.rules));
|
|
1016
|
+
const betterleaksCredits = Object.assign({}, ...betterleaks.map((item) => item.credits));
|
|
1017
|
+
const betterleaksProvenance = Object.assign({}, ...betterleaks.map((item) => item.provenance));
|
|
1018
|
+
const qualification = qualificationEvidence(sourceRoot, tracked);
|
|
1019
|
+
const selected = selectedRuntimeCatalog(sourceRoot, tracked);
|
|
1020
|
+
return {
|
|
1021
|
+
format: 'lattice-security-catalog-v1',
|
|
1022
|
+
...(qualification ? { qualificationEvidence: qualification } : {}),
|
|
1023
|
+
...(selected ? { selectedRuntime: selected.contract } : {}),
|
|
1024
|
+
...(release ? { commercialRelease: { record: release.ref, evidence: release.evidence, checkoutEnabled: false, paidPublicationEnabled: false } } : {}),
|
|
1025
|
+
rules: Object.fromEntries(Object.entries({ ...original.rules, ...builtins.rules, ...betterleaksRules, ...(selected?.rules ?? {}) }).sort(([a], [b]) => a.localeCompare(b))),
|
|
1026
|
+
credits: {
|
|
1027
|
+
'm2ai:vsg-original': {
|
|
1028
|
+
attributionId: 'm2ai:vsg-original',
|
|
1029
|
+
displayName: 'Vibe Security Gate original code and rules, M2 AI, LLC',
|
|
1030
|
+
sourceUrl: originalSourceArchiveUrl ?? `https://github.com/rmach816/vibe-security-gate/tree/${sourceCommit}`,
|
|
1031
|
+
licenseName: 'Apache-2.0',
|
|
1032
|
+
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
1033
|
+
modificationNotice: null,
|
|
1034
|
+
},
|
|
1035
|
+
...betterleaksCredits,
|
|
1036
|
+
...(selected?.credits ?? {}),
|
|
1037
|
+
},
|
|
1038
|
+
provenance: {
|
|
1039
|
+
sourceCommit,
|
|
1040
|
+
rules: Object.fromEntries(Object.entries({ ...original.provenance, ...builtins.provenance, ...betterleaksProvenance, ...(selected?.provenance ?? {}) }).sort(([a], [b]) => a.localeCompare(b))),
|
|
1041
|
+
},
|
|
1042
|
+
candidateBlockers: betterleaks.flatMap((item) => item.blockers),
|
|
1043
|
+
checkClasses: [
|
|
1044
|
+
'agent-review', 'agent-surface', 'code', 'dep-change', 'deps-vuln', 'licenses', 'live-basic',
|
|
1045
|
+
'live-js-secrets', 'malware', 'project-scripts', 'sbom', 'secrets-history', 'secrets-tree',
|
|
1046
|
+
'supabase-static', 'workflows',
|
|
1047
|
+
],
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function parseRuntimeManifest(source) {
|
|
1052
|
+
const match = /^export default \{manifest: (\{[^\r\n]+\}),\r?\n/.exec(source);
|
|
1053
|
+
if (!match) fail('the evaluator must begin with its default-export manifest.');
|
|
1054
|
+
try { return JSON.parse(match[1]); } catch { fail('the evaluator manifest is invalid.'); }
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function copyFile(source, target) {
|
|
1058
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1059
|
+
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function walkArtifact(root, dir = root, rel = '') {
|
|
1063
|
+
const files = [];
|
|
1064
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1065
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1066
|
+
const child = path.join(dir, entry.name);
|
|
1067
|
+
const stat = fs.lstatSync(child);
|
|
1068
|
+
if (stat.isSymbolicLink()) fail(`linked path ${childRel} is forbidden.`);
|
|
1069
|
+
if (entry.isDirectory()) files.push(...walkArtifact(root, child, childRel));
|
|
1070
|
+
else if (entry.isFile()) files.push(childRel);
|
|
1071
|
+
else fail(`non-file path ${childRel} is forbidden.`);
|
|
1072
|
+
}
|
|
1073
|
+
return files;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** Builds a blocked candidate or owner-approved artifact from one immutable clean Git source commit. */
|
|
1077
|
+
export function buildLatticeArtifacts({ sourceRoot, outDir, originalSourceArchiveUrl }) {
|
|
1078
|
+
validateOriginalSourceArchiveUrl(originalSourceArchiveUrl);
|
|
1079
|
+
sourceRoot = fs.realpathSync(path.resolve(sourceRoot));
|
|
1080
|
+
outDir = path.resolve(outDir);
|
|
1081
|
+
const top = git(sourceRoot, ['rev-parse', '--show-toplevel']);
|
|
1082
|
+
if (path.resolve(top) !== path.resolve(sourceRoot)) fail('sourceRoot must be the Git worktree root.');
|
|
1083
|
+
const status = git(sourceRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1084
|
+
if (status) fail('sourceRoot must be clean before artifact generation.');
|
|
1085
|
+
const sourceCommit = git(sourceRoot, ['rev-parse', 'HEAD']);
|
|
1086
|
+
if (!/^[0-9a-f]{40}$/.test(sourceCommit)) fail('source commit is invalid.');
|
|
1087
|
+
if (fs.existsSync(outDir)) fail('outDir must not already exist.');
|
|
1088
|
+
|
|
1089
|
+
const tracked = trackedFiles(sourceRoot);
|
|
1090
|
+
const allowlist = sourceAllowlist(sourceRoot, tracked);
|
|
1091
|
+
const release = commercialReleaseEvidence(sourceRoot, tracked, allowlist);
|
|
1092
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8'));
|
|
1093
|
+
const commercialManifestDigest = sha256(fs.readFileSync(path.join(sourceRoot, 'licensing', 'commercial-components.json')));
|
|
1094
|
+
const baseIdentity = {
|
|
1095
|
+
vsgVersion: packageJson.version,
|
|
1096
|
+
profileId: release ? 'lattice-security-commercial' : 'lattice-candidate-blocked',
|
|
1097
|
+
profileVersion: '1.0.0',
|
|
1098
|
+
rulesDigest: hashDirectory(sourceRoot, 'rules/', allowlist),
|
|
1099
|
+
registryDigest: hashDirectory(sourceRoot, 'tools/registry/', tracked),
|
|
1100
|
+
commercialManifestDigest,
|
|
1101
|
+
};
|
|
1102
|
+
const catalog = runtimeCatalog(sourceRoot, tracked, sourceCommit, release, originalSourceArchiveUrl);
|
|
1103
|
+
const releaseEligibility = release ? {
|
|
1104
|
+
status: 'approved',
|
|
1105
|
+
blockers: [],
|
|
1106
|
+
approval: { owner: release.record.owner, decidedOn: release.record.decidedOn, record: release.ref },
|
|
1107
|
+
checkoutEnabled: false,
|
|
1108
|
+
paidPublicationEnabled: false,
|
|
1109
|
+
} : { status: 'blocked', blockers: RELEASE_BLOCKERS };
|
|
1110
|
+
const runtime = buildLatticeRuntimeSource({ identity: baseIdentity, catalog, releaseEligibility });
|
|
1111
|
+
const { format: _format, releaseEligibility: _runtimeEligibility, ...identity } = runtime.manifest;
|
|
1112
|
+
const staging = fs.mkdtempSync(path.join(path.dirname(outDir), `${path.basename(outDir)}-staging-`));
|
|
1113
|
+
try {
|
|
1114
|
+
for (const rel of allowlist) copyFile(path.join(sourceRoot, ...rel.split('/')), path.join(staging, ...rel.split('/')));
|
|
1115
|
+
fs.writeFileSync(path.join(staging, EVALUATOR_PATH), runtime.source, { encoding: 'utf8', flag: 'wx' });
|
|
1116
|
+
fs.writeFileSync(path.join(staging, 'lattice-catalog.json'), `${JSON.stringify(catalog, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1117
|
+
const artifactFiles = [...allowlist, EVALUATOR_PATH, 'lattice-catalog.json'].sort();
|
|
1118
|
+
const files = artifactFiles.map((rel) => {
|
|
1119
|
+
const bytes = fs.readFileSync(path.join(staging, ...rel.split('/')));
|
|
1120
|
+
return { path: rel, sha256: sha256(bytes), bytes: bytes.byteLength };
|
|
1121
|
+
});
|
|
1122
|
+
const manifest = {
|
|
1123
|
+
format: FORMAT,
|
|
1124
|
+
sourceCommit,
|
|
1125
|
+
identity,
|
|
1126
|
+
entryPoint: ENTRY_POINT,
|
|
1127
|
+
exporterPath: EXPORTER_PATH,
|
|
1128
|
+
transportPath: TRANSPORT_PATH,
|
|
1129
|
+
evaluatorPath: EVALUATOR_PATH,
|
|
1130
|
+
releaseEligibility,
|
|
1131
|
+
files,
|
|
1132
|
+
};
|
|
1133
|
+
fs.writeFileSync(path.join(staging, 'artifact-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1134
|
+
fs.renameSync(staging, outDir);
|
|
1135
|
+
return manifest;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** Reads and fully re-verifies an already built Lattice artifact without executing it. */
|
|
1143
|
+
export function readLatticeArtifact({ artifactRoot }) {
|
|
1144
|
+
artifactRoot = fs.realpathSync(path.resolve(artifactRoot));
|
|
1145
|
+
const manifestPath = path.join(artifactRoot, 'artifact-manifest.json');
|
|
1146
|
+
let manifest;
|
|
1147
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch (error) { fail(`artifact-manifest.json is missing or invalid (${error.message}).`); }
|
|
1148
|
+
const keys = ['format', 'sourceCommit', 'identity', 'entryPoint', 'exporterPath', 'transportPath', 'evaluatorPath', 'releaseEligibility', 'files'];
|
|
1149
|
+
const extra = Object.keys(manifest).find((key) => !keys.includes(key));
|
|
1150
|
+
if (extra || manifest.format !== FORMAT) fail(extra ? `manifest field ${extra} is not allowed.` : 'manifest format is unsupported.');
|
|
1151
|
+
if (!/^[0-9a-f]{40}$/.test(manifest.sourceCommit ?? '')) fail('sourceCommit is invalid.');
|
|
1152
|
+
if (manifest.entryPoint !== ENTRY_POINT || manifest.exporterPath !== EXPORTER_PATH || manifest.transportPath !== TRANSPORT_PATH || manifest.evaluatorPath !== EVALUATOR_PATH) fail('runtime paths do not match the fixed interface.');
|
|
1153
|
+
if (!['blocked', 'approved'].includes(manifest.releaseEligibility?.status)) fail('artifact release eligibility is unsupported.');
|
|
1154
|
+
if (!Array.isArray(manifest.files) || manifest.files.length === 0) fail('manifest files are missing.');
|
|
1155
|
+
const names = new Set();
|
|
1156
|
+
const lower = new Set();
|
|
1157
|
+
for (const item of manifest.files) {
|
|
1158
|
+
if (!item || !safeRelative(item.path) || names.has(item.path) || lower.has(item.path.toLowerCase())) fail(`file path ${item?.path ?? ''} is unsafe, duplicate, or case-colliding.`);
|
|
1159
|
+
if (RULE_TEST_SPECIMEN.test(item.path)) fail(`rule test specimen ${item.path} is not allowed in a runtime artifact.`);
|
|
1160
|
+
names.add(item.path); lower.add(item.path.toLowerCase());
|
|
1161
|
+
if (!SHA256.test(item.sha256 ?? '') || !Number.isSafeInteger(item.bytes) || item.bytes < 0) fail(`file metadata for ${item.path} is invalid.`);
|
|
1162
|
+
const file = path.join(artifactRoot, ...item.path.split('/'));
|
|
1163
|
+
const stat = fs.lstatSync(file);
|
|
1164
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`declared path ${item.path} is not a regular file.`);
|
|
1165
|
+
const bytes = fs.readFileSync(file);
|
|
1166
|
+
if (bytes.byteLength !== item.bytes || sha256(bytes) !== item.sha256) fail(`declared file ${item.path} was tampered with.`);
|
|
1167
|
+
}
|
|
1168
|
+
const actual = walkArtifact(artifactRoot).filter((rel) => rel !== 'artifact-manifest.json').sort();
|
|
1169
|
+
if (actual.length !== names.size || actual.some((rel) => !names.has(rel))) fail('artifact contains an undeclared file.');
|
|
1170
|
+
if (manifest.files.some((item, index) => index > 0 && manifest.files[index - 1].path >= item.path)) fail('manifest files must use deterministic path order.');
|
|
1171
|
+
const runtimeManifest = parseRuntimeManifest(fs.readFileSync(path.join(artifactRoot, EVALUATOR_PATH), 'utf8'));
|
|
1172
|
+
if (canonical(runtimeManifest) !== canonical({ format: 'lattice-security-runtime-v1', ...manifest.identity, releaseEligibility: manifest.releaseEligibility })) fail('evaluator identity does not match the artifact manifest.');
|
|
1173
|
+
for (const key of ['rulesDigest', 'registryDigest', 'commercialManifestDigest', 'evaluatorDigest', 'catalogDigest']) {
|
|
1174
|
+
if (!SHA256.test(manifest.identity?.[key] ?? '')) fail(`identity ${key} is invalid.`);
|
|
1175
|
+
}
|
|
1176
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(artifactRoot, 'package.json'), 'utf8'));
|
|
1177
|
+
if (packageJson.version !== manifest.identity.vsgVersion) fail('VSG version does not match packaged metadata.');
|
|
1178
|
+
if (sha256(fs.readFileSync(path.join(artifactRoot, 'licensing', 'commercial-components.json'))) !== manifest.identity.commercialManifestDigest) fail('commercial manifest identity does not match its bytes.');
|
|
1179
|
+
const paths = manifest.files.map((item) => item.path);
|
|
1180
|
+
const release = commercialReleaseEvidence(artifactRoot, paths, paths);
|
|
1181
|
+
const expectedEligibility = release ? {
|
|
1182
|
+
status: 'approved',
|
|
1183
|
+
blockers: [],
|
|
1184
|
+
approval: { owner: release.record.owner, decidedOn: release.record.decidedOn, record: release.ref },
|
|
1185
|
+
checkoutEnabled: false,
|
|
1186
|
+
paidPublicationEnabled: false,
|
|
1187
|
+
} : { status: 'blocked', blockers: RELEASE_BLOCKERS };
|
|
1188
|
+
if (canonical(manifest.releaseEligibility) !== canonical(expectedEligibility)) fail('artifact release eligibility does not match its reviewed release record.');
|
|
1189
|
+
if (hashDirectory(artifactRoot, 'rules/', paths) !== manifest.identity.rulesDigest) fail('rules identity does not match packaged bytes.');
|
|
1190
|
+
if (hashDirectory(artifactRoot, 'tools/registry/', paths) !== manifest.identity.registryDigest) fail('registry identity does not match packaged bytes.');
|
|
1191
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(artifactRoot, 'lattice-catalog.json'), 'utf8'));
|
|
1192
|
+
if (sha256(canonical(catalog)) !== manifest.identity.catalogDigest) fail('catalog identity does not match its bytes.');
|
|
1193
|
+
if (canonical(catalog.qualificationEvidence ?? null) !== canonical(qualificationEvidence(artifactRoot, paths))) {
|
|
1194
|
+
fail('catalog qualification evidence does not match packaged records.');
|
|
1195
|
+
}
|
|
1196
|
+
const expectedReleaseCatalog = release ? { record: release.ref, evidence: release.evidence, checkoutEnabled: false, paidPublicationEnabled: false } : null;
|
|
1197
|
+
if (canonical(catalog.commercialRelease ?? null) !== canonical(expectedReleaseCatalog)) {
|
|
1198
|
+
fail('catalog commercial release evidence does not match packaged records.');
|
|
1199
|
+
}
|
|
1200
|
+
return manifest;
|
|
1201
|
+
}
|