@securityreviewai/vibereview-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +189 -0
  3. package/SECURITY.md +46 -0
  4. package/bin/vibereview.js +7 -0
  5. package/dist/catalog/guardrails.br +0 -0
  6. package/dist/src/cli.js +103 -0
  7. package/dist/src/commands/generate.js +36 -0
  8. package/dist/src/commands/init.js +142 -0
  9. package/dist/src/core/assets.js +19 -0
  10. package/dist/src/core/catalog.js +51 -0
  11. package/dist/src/core/code-guardrails.js +154 -0
  12. package/dist/src/core/detector.js +214 -0
  13. package/dist/src/core/evidence.js +107 -0
  14. package/dist/src/core/fs.js +16 -0
  15. package/dist/src/core/generation-prompt.js +35 -0
  16. package/dist/src/core/generator.js +42 -0
  17. package/dist/src/core/hash.js +18 -0
  18. package/dist/src/core/integration.js +197 -0
  19. package/dist/src/core/matcher.js +33 -0
  20. package/dist/src/core/repository.js +36 -0
  21. package/dist/src/core/workspace.js +157 -0
  22. package/dist/src/providers/claude.js +27 -0
  23. package/dist/src/providers/codex.js +27 -0
  24. package/dist/src/providers/copilot.js +44 -0
  25. package/dist/src/providers/cursor.js +27 -0
  26. package/dist/src/providers/index.js +23 -0
  27. package/dist/src/providers/process.js +16 -0
  28. package/dist/src/providers/types.js +2 -0
  29. package/dist/src/runners/claude.js +22 -0
  30. package/dist/src/runners/codex.js +25 -0
  31. package/dist/src/runners/copilot.js +21 -0
  32. package/dist/src/runners/cursor.js +20 -0
  33. package/dist/src/runners/index.js +26 -0
  34. package/dist/src/runners/process.js +34 -0
  35. package/dist/src/runners/types.js +2 -0
  36. package/dist/src/types.js +2 -0
  37. package/dist/src/ui.js +22 -0
  38. package/docs/provider-contracts.md +55 -0
  39. package/package.json +49 -0
  40. package/runtime/hook-context.cjs +44 -0
  41. package/schemas/code-guardrails-v1.json +45 -0
  42. package/skills/guardrail-generator/SKILL.md +237 -0
  43. package/skills/guardrail-generator/agents/openai.yaml +3 -0
  44. package/skills/guardrail-generator/evals/evals.json +23 -0
  45. package/skills/guardrail-generator/references/output-contract.md +40 -0
  46. package/skills/vibereview-guardrails/SKILL.md +58 -0
  47. package/skills/vibereview-guardrails/agents/openai.yaml +3 -0
  48. package/skills/vibereview-osv-scan/SKILL.md +60 -0
  49. package/skills/vibereview-osv-scan/agents/openai.yaml +3 -0
  50. package/skills/vibereview-osv-scan/scripts/osv-scan.mjs +77 -0
  51. package/skills/vibereview-report/SKILL.md +49 -0
  52. package/skills/vibereview-report/agents/openai.yaml +3 -0
  53. package/skills/vibereview-report/references/report-contract.md +100 -0
  54. package/skills/vibereview-secure-code/SKILL.md +57 -0
  55. package/skills/vibereview-secure-code/agents/openai.yaml +3 -0
  56. package/skills/vibereview-threat-model/SKILL.md +60 -0
  57. package/skills/vibereview-threat-model/agents/openai.yaml +3 -0
  58. package/skills/vibereview-threat-model/references/pwnisms.md +46 -0
@@ -0,0 +1,142 @@
1
+ import path from "node:path";
2
+ import { basename } from "node:path";
3
+ import { access } from "node:fs/promises";
4
+ import { select } from "@inquirer/prompts";
5
+ import { loadCatalog } from "../core/catalog.js";
6
+ import { detectTechnologyProfile } from "../core/detector.js";
7
+ import { sha256, stableJson } from "../core/hash.js";
8
+ import { matchGuardrails } from "../core/matcher.js";
9
+ import { findRepositoryRoot } from "../core/repository.js";
10
+ import { initializeWorkspace } from "../core/workspace.js";
11
+ import { generateCodeGuardrails } from "../core/generator.js";
12
+ import { installProviderIntegration } from "../core/integration.js";
13
+ import { checkProviderPreflight, isProvider, PROVIDER_NAMES, PROVIDERS } from "../providers/index.js";
14
+ import { ui } from "../ui.js";
15
+ export async function initCommand(options) {
16
+ const root = await findRepositoryRoot(options.cwd);
17
+ const projectName = basename(root);
18
+ if (!options.json)
19
+ ui.title("VibeReview");
20
+ const provider = await resolveProvider(options);
21
+ if (!options.force && await exists(path.join(root, ".vibereview", "config.json"))) {
22
+ throw new Error("VibeReview is already initialized. Re-run with `--force` to regenerate its managed files.");
23
+ }
24
+ const preflight = options.skipPreflight ? undefined : await checkProviderPreflight(provider);
25
+ if (!options.json) {
26
+ ui.step(`${PROVIDER_NAMES[provider]} CLI detected${preflight ? ` (${preflight.version})` : " (preflight skipped)"}`);
27
+ if (preflight) {
28
+ if (preflight.authentication === "deferred")
29
+ ui.warning(preflight.authenticationMessage);
30
+ else
31
+ ui.step(preflight.authenticationMessage);
32
+ }
33
+ ui.info("\nAnalyzing workspace...");
34
+ }
35
+ const catalog = await loadCatalog();
36
+ const profile = await detectTechnologyProfile(root, projectName);
37
+ const matches = matchGuardrails(profile, catalog);
38
+ profile.matched_packs = matches.packs.map((pack) => pack.slug);
39
+ const technologies = profile.technologies.map((item) => item.name);
40
+ if (!options.json) {
41
+ ui.step(technologies.length ? `Detected ${formatList(technologies)}` : "No supported technologies detected");
42
+ ui.step(`Matched ${matches.packs.length} guardrail pack${matches.packs.length === 1 ? "" : "s"}`);
43
+ ui.step(`Selected ${matches.guardrails.length} baseline guardrail${matches.guardrails.length === 1 ? "" : "s"}`);
44
+ if (options.verbose) {
45
+ for (const pack of matches.packs)
46
+ ui.detail(` ${pack.slug}: ${pack.guardrails.length} catalog rules`);
47
+ for (const warning of profile.warnings)
48
+ ui.warning(warning);
49
+ if (matches.unmatchedTechnologies.length)
50
+ ui.detail(` No bundled pack: ${matches.unmatchedTechnologies.join(", ")}`);
51
+ }
52
+ }
53
+ let generation;
54
+ let generationError;
55
+ if (!options.skipGeneration) {
56
+ if (!options.json)
57
+ ui.info(`\nGenerating code-specific guardrails with ${PROVIDER_NAMES[provider]}...`);
58
+ try {
59
+ generation = await generateCodeGuardrails(provider, root, profile, matches.guardrails);
60
+ if (!options.json) {
61
+ ui.step(`Analyzed ${generation.evidence.files.length} security-relevant file${generation.evidence.files.length === 1 ? "" : "s"}`);
62
+ ui.step(`Generated ${generation.guardrails.length} code-specific guardrail${generation.guardrails.length === 1 ? "" : "s"}`);
63
+ if (options.verbose && generation.attempts > 1)
64
+ ui.detail(" Structured output was repaired on the second attempt.");
65
+ }
66
+ }
67
+ catch (error) {
68
+ generationError = error instanceof Error ? error.message : String(error);
69
+ if (!options.json) {
70
+ ui.warning("Code-specific generation failed; baseline guardrails will still be installed.");
71
+ if (options.verbose)
72
+ ui.detail(` ${generationError}`);
73
+ ui.detail(" Retry later with: vibereview guardrails generate");
74
+ }
75
+ }
76
+ }
77
+ const written = await initializeWorkspace({
78
+ root,
79
+ projectName,
80
+ provider,
81
+ profile,
82
+ guardrails: matches.guardrails,
83
+ ...(generation ? { codeSpecific: generation.guardrails } : {}),
84
+ catalogHash: sha256(stableJson(catalog)),
85
+ force: options.force,
86
+ });
87
+ const integrationFiles = await installProviderIntegration({ root, projectName, provider });
88
+ written.push(...integrationFiles);
89
+ if (options.json) {
90
+ process.stdout.write(`${JSON.stringify({
91
+ project_name: projectName,
92
+ provider,
93
+ technologies,
94
+ packs: matches.packs.map((p) => p.slug),
95
+ baseline_guardrail_count: matches.guardrails.length,
96
+ code_specific_guardrail_count: generation?.guardrails.length ?? 0,
97
+ generation_status: options.skipGeneration ? "skipped" : generation ? "completed" : "failed",
98
+ ...(generationError ? { generation_error: generationError } : {}),
99
+ written,
100
+ }, null, 2)}\n`);
101
+ return;
102
+ }
103
+ ui.info("\nWorkspace configured.");
104
+ ui.detail(`Profile: ${path.relative(root, path.join(root, ".vibereview", "profile.json"))}`);
105
+ ui.detail(`Guardrails: ${path.relative(root, path.join(root, ".vibereview", "guardrails.yml"))}`);
106
+ ui.step(`Installed local security workflow for ${PROVIDER_NAMES[provider]}`);
107
+ ui.detail(`Reports: ${path.relative(root, path.join(root, ".vibereview", "reports"))}/*.md`);
108
+ ui.info("\nVibeReview is ready. Restart the IDE agent if it is already running.\n");
109
+ }
110
+ async function exists(filePath) {
111
+ try {
112
+ await access(filePath);
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ async function resolveProvider(options) {
120
+ const requested = options.provider;
121
+ if (requested) {
122
+ if (!isProvider(requested)) {
123
+ throw new Error(`Unknown provider ${JSON.stringify(requested)}. Choose one of: ${PROVIDERS.join(", ")}.`);
124
+ }
125
+ return requested;
126
+ }
127
+ if (options.yes) {
128
+ throw new Error("`--yes` requires `--provider cursor|codex|claude|copilot`.");
129
+ }
130
+ return select({
131
+ message: "Which coding agent do you use?",
132
+ choices: PROVIDERS.map((provider) => ({ name: PROVIDER_NAMES[provider], value: provider })),
133
+ });
134
+ }
135
+ function formatList(items) {
136
+ if (items.length <= 1)
137
+ return items.join("");
138
+ if (items.length === 2)
139
+ return `${items[0]} and ${items[1]}`;
140
+ return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
141
+ }
142
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,19 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export async function loadGuardrailGeneratorAssets() {
5
+ const root = packageRoot();
6
+ const skillPath = path.join(root, "skills", "guardrail-generator", "SKILL.md");
7
+ const outputContractPath = path.join(root, "skills", "guardrail-generator", "references", "output-contract.md");
8
+ const schemaPath = path.join(root, "schemas", "code-guardrails-v1.json");
9
+ const [skill, outputContract, schemaText] = await Promise.all([
10
+ readFile(skillPath, "utf8"),
11
+ readFile(outputContractPath, "utf8"),
12
+ readFile(schemaPath, "utf8"),
13
+ ]);
14
+ return { skill, outputContract, schema: JSON.parse(schemaText), schemaPath };
15
+ }
16
+ function packageRoot() {
17
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
18
+ }
19
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { brotliDecompressSync } from "node:zlib";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ export async function loadCatalog(catalogPath = defaultCatalogPath()) {
6
+ const compressed = await readFile(catalogPath);
7
+ const value = JSON.parse(brotliDecompressSync(compressed).toString("utf8"));
8
+ return validateCatalog(value);
9
+ }
10
+ export function validateCatalog(value) {
11
+ if (!isRecord(value) || value.schema_version !== "1" || !Array.isArray(value.packs)) {
12
+ throw new Error("invalid embedded guardrail catalog");
13
+ }
14
+ for (const pack of value.packs)
15
+ validatePack(pack);
16
+ return value;
17
+ }
18
+ function validatePack(value) {
19
+ if (!isRecord(value))
20
+ throw new Error("guardrail pack must be an object");
21
+ for (const key of ["slug", "technology", "description"]) {
22
+ if (typeof value[key] !== "string" || value[key].trim() === "") {
23
+ throw new Error(`guardrail pack ${key} must be a non-empty string`);
24
+ }
25
+ }
26
+ if (!Array.isArray(value.keywords) || !value.keywords.every((item) => typeof item === "string")) {
27
+ throw new Error(`guardrail pack ${value.slug} has invalid keywords`);
28
+ }
29
+ if (!Array.isArray(value.guardrails))
30
+ throw new Error(`guardrail pack ${value.slug} has no guardrails`);
31
+ for (const guardrail of value.guardrails) {
32
+ if (!isRecord(guardrail))
33
+ throw new Error(`guardrail pack ${value.slug} contains a non-object rule`);
34
+ for (const key of ["title", "category", "instruction"]) {
35
+ if (typeof guardrail[key] !== "string" || guardrail[key].trim() === "") {
36
+ throw new Error(`guardrail pack ${value.slug} contains a rule with invalid ${key}`);
37
+ }
38
+ }
39
+ if (!["do", "dont", "must", "must_not"].includes(String(guardrail.rule_type))) {
40
+ throw new Error(`guardrail pack ${value.slug} contains an invalid rule_type`);
41
+ }
42
+ }
43
+ }
44
+ function defaultCatalogPath() {
45
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
46
+ return path.resolve(currentDir, "..", "..", "catalog", "guardrails.br");
47
+ }
48
+ function isRecord(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,154 @@
1
+ import { sha256 } from "./hash.js";
2
+ export class GuardrailOutputError extends Error {
3
+ name = "GuardrailOutputError";
4
+ }
5
+ export function parseGeneratedGuardrails(raw, allowedEvidencePaths) {
6
+ const value = unwrapProviderOutput(raw);
7
+ return validateOutput(value, allowedEvidencePaths);
8
+ }
9
+ export function normalizeGeneratedGuardrails(output, baseline) {
10
+ const baselineFingerprints = new Set(baseline.map(ruleFingerprint));
11
+ const generated = new Map();
12
+ for (const rule of output.guardrails) {
13
+ const fingerprint = ruleFingerprint(rule);
14
+ if (baselineFingerprints.has(fingerprint) || generated.has(fingerprint))
15
+ continue;
16
+ const idPart = sha256(`${rule.type}\0${rule.category}\0${rule.instruction}`).slice(0, 16);
17
+ generated.set(fingerprint, {
18
+ id: `code:${slugify(rule.title)}:${idPart}`,
19
+ source: "code_generated",
20
+ type: rule.type,
21
+ title: rule.title.trim(),
22
+ category: rule.category.trim(),
23
+ instruction: rule.instruction.trim(),
24
+ rationale: rule.rationale.trim(),
25
+ confidence: rule.confidence,
26
+ evidence: rule.evidence,
27
+ ...(rule.cwe_ids?.length ? { cwe_ids: unique(rule.cwe_ids) } : {}),
28
+ ...(rule.owasp_top10?.length ? { owasp_top10: unique(rule.owasp_top10) } : {}),
29
+ });
30
+ }
31
+ return [...generated.values()].sort((left, right) => left.category.localeCompare(right.category) || left.title.localeCompare(right.title));
32
+ }
33
+ function unwrapProviderOutput(raw) {
34
+ const trimmed = raw.trim();
35
+ if (!trimmed)
36
+ throw new GuardrailOutputError("provider returned an empty response");
37
+ const direct = tryJson(trimmed);
38
+ if (direct !== undefined)
39
+ return unwrapKnownWrapper(direct);
40
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
41
+ if (fenced) {
42
+ const value = tryJson(fenced[1].trim());
43
+ if (value !== undefined)
44
+ return unwrapKnownWrapper(value);
45
+ }
46
+ const start = trimmed.indexOf("{");
47
+ const end = trimmed.lastIndexOf("}");
48
+ if (start >= 0 && end > start) {
49
+ const value = tryJson(trimmed.slice(start, end + 1));
50
+ if (value !== undefined)
51
+ return unwrapKnownWrapper(value);
52
+ }
53
+ throw new GuardrailOutputError("provider response did not contain valid JSON");
54
+ }
55
+ function unwrapKnownWrapper(value) {
56
+ if (!isRecord(value))
57
+ return value;
58
+ if (isRecord(value.structured_output))
59
+ return value.structured_output;
60
+ if (isRecord(value.result))
61
+ return value.result;
62
+ if (typeof value.result === "string") {
63
+ const nested = tryJson(value.result.trim());
64
+ if (nested !== undefined)
65
+ return nested;
66
+ }
67
+ return value;
68
+ }
69
+ function validateOutput(value, allowedPaths) {
70
+ if (!isRecord(value))
71
+ throw new GuardrailOutputError("output must be an object");
72
+ assertExactKeys(value, ["schema_version", "summary", "guardrails"], "output");
73
+ if (value.schema_version !== "1")
74
+ throw new GuardrailOutputError("schema_version must be \"1\"");
75
+ const summary = nonEmptyString(value.summary, "summary", 1000);
76
+ if (!Array.isArray(value.guardrails) || value.guardrails.length > 20) {
77
+ throw new GuardrailOutputError("guardrails must be an array with at most 20 items");
78
+ }
79
+ const guardrails = value.guardrails.map((item, index) => validateRule(item, index, allowedPaths));
80
+ return { schema_version: "1", summary, guardrails };
81
+ }
82
+ function validateRule(value, index, allowedPaths) {
83
+ if (!isRecord(value))
84
+ throw new GuardrailOutputError(`guardrails[${index}] must be an object`);
85
+ assertExactKeys(value, ["title", "type", "category", "instruction", "rationale", "confidence", "evidence", "cwe_ids", "owasp_top10"], `guardrails[${index}]`);
86
+ if (value.type !== "must" && value.type !== "must_not")
87
+ throw new GuardrailOutputError(`guardrails[${index}].type is invalid`);
88
+ if (value.confidence !== "high" && value.confidence !== "medium")
89
+ throw new GuardrailOutputError(`guardrails[${index}].confidence is invalid`);
90
+ if (!Array.isArray(value.evidence) || value.evidence.length === 0 || value.evidence.length > 10) {
91
+ throw new GuardrailOutputError(`guardrails[${index}].evidence must contain 1-10 items`);
92
+ }
93
+ const evidence = value.evidence.map((item, evidenceIndex) => validateEvidence(item, index, evidenceIndex, allowedPaths));
94
+ return {
95
+ title: nonEmptyString(value.title, `guardrails[${index}].title`, 200),
96
+ type: value.type,
97
+ category: nonEmptyString(value.category, `guardrails[${index}].category`, 100),
98
+ instruction: nonEmptyString(value.instruction, `guardrails[${index}].instruction`, 1000),
99
+ rationale: nonEmptyString(value.rationale, `guardrails[${index}].rationale`, 1000),
100
+ confidence: value.confidence,
101
+ evidence,
102
+ cwe_ids: stringArray(value.cwe_ids, `guardrails[${index}].cwe_ids`, /^\d+$/),
103
+ owasp_top10: stringArray(value.owasp_top10, `guardrails[${index}].owasp_top10`),
104
+ };
105
+ }
106
+ function validateEvidence(value, ruleIndex, evidenceIndex, allowedPaths) {
107
+ const label = `guardrails[${ruleIndex}].evidence[${evidenceIndex}]`;
108
+ if (!isRecord(value))
109
+ throw new GuardrailOutputError(`${label} must be an object`);
110
+ assertExactKeys(value, ["path", "reason"], label);
111
+ const evidencePath = nonEmptyString(value.path, `${label}.path`, 500);
112
+ if (!allowedPaths.has(evidencePath))
113
+ throw new GuardrailOutputError(`${label}.path was not supplied as evidence: ${evidencePath}`);
114
+ return { path: evidencePath, reason: nonEmptyString(value.reason, `${label}.reason`, 500) };
115
+ }
116
+ function assertExactKeys(value, allowed, label) {
117
+ const allowedSet = new Set(allowed);
118
+ const extra = Object.keys(value).filter((key) => !allowedSet.has(key));
119
+ if (extra.length)
120
+ throw new GuardrailOutputError(`${label} contains unsupported fields: ${extra.join(", ")}`);
121
+ }
122
+ function nonEmptyString(value, label, maxLength) {
123
+ if (typeof value !== "string" || value.trim() === "" || value.length > maxLength) {
124
+ throw new GuardrailOutputError(`${label} must be a non-empty string no longer than ${maxLength} characters`);
125
+ }
126
+ return value.trim();
127
+ }
128
+ function stringArray(value, label, pattern) {
129
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && (!pattern || pattern.test(item)))) {
130
+ throw new GuardrailOutputError(`${label} must be a valid string array`);
131
+ }
132
+ return unique(value);
133
+ }
134
+ function ruleFingerprint(rule) {
135
+ return `${rule.type}\0${rule.category.trim().toLowerCase()}\0${rule.instruction.trim().toLowerCase().replace(/\s+/g, " ")}`;
136
+ }
137
+ function slugify(value) {
138
+ return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "guardrail";
139
+ }
140
+ function unique(values) {
141
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
142
+ }
143
+ function tryJson(value) {
144
+ try {
145
+ return JSON.parse(value);
146
+ }
147
+ catch {
148
+ return undefined;
149
+ }
150
+ }
151
+ function isRecord(value) {
152
+ return typeof value === "object" && value !== null && !Array.isArray(value);
153
+ }
154
+ //# sourceMappingURL=code-guardrails.js.map
@@ -0,0 +1,214 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const EXCLUDED_DIRS = new Set([
4
+ ".git", ".vibereview", "node_modules", "vendor", "dist", "build", ".next", ".turbo",
5
+ "coverage", ".venv", "venv", "target", "Pods",
6
+ ]);
7
+ const MAX_FILES = 50_000;
8
+ const MAX_MANIFEST_BYTES = 2_000_000;
9
+ const DISPLAY_NAMES = {
10
+ typescript: "TypeScript", javascript: "JavaScript", nodejs: "Node.js", nextjs: "Next.js",
11
+ react: "React", vuejs: "Vue.js", angular: "Angular", express: "Express", fastify: "Fastify",
12
+ nestjs: "NestJS", honojs: "Hono", python: "Python", django: "Django", flask: "Flask",
13
+ fastapi: "FastAPI", go: "Go", rust: "Rust", java: "Java", "java-spring-boot": "Spring Boot",
14
+ postgresql: "PostgreSQL", "mysql-mariadb": "MySQL/MariaDB", mongodb: "MongoDB", redis: "Redis",
15
+ docker: "Docker", kubernetes: "Kubernetes", "github-actions": "GitHub Actions",
16
+ "terraform-aws": "Terraform/AWS", "terraform-azure": "Terraform/Azure",
17
+ "terraform-gcp": "Terraform/GCP", "oauth-oidc": "OAuth/OIDC", jwt: "JWT",
18
+ "openapi-rest-api": "OpenAPI/REST", grpc: "gRPC", websockets: "WebSockets",
19
+ "mcp-server-hardening": "MCP server", langchain: "LangChain", langgraph: "LangGraph",
20
+ "openai-agents-sdk": "OpenAI Agents SDK",
21
+ };
22
+ const DEPENDENCY_RULES = {
23
+ typescript: ["typescript", "ts-node", "tsx"],
24
+ nextjs: ["next"], react: ["react", "react-dom"], vuejs: ["vue"], angular: ["@angular/core"],
25
+ express: ["express"], fastify: ["fastify"], nestjs: ["@nestjs/core"], honojs: ["hono"],
26
+ postgresql: ["pg", "postgres", "@prisma/adapter-pg"],
27
+ "mysql-mariadb": ["mysql", "mysql2", "mariadb"], mongodb: ["mongodb", "mongoose"], redis: ["redis", "ioredis"],
28
+ "oauth-oidc": ["next-auth", "@auth/core", "passport", "openid-client", "@auth0/nextjs-auth0"],
29
+ jwt: ["jsonwebtoken", "jose", "jwt-decode"],
30
+ websockets: ["ws", "socket.io"], grpc: ["@grpc/grpc-js"],
31
+ "mcp-server-hardening": ["@modelcontextprotocol/sdk"],
32
+ langchain: ["langchain", "@langchain/core"], langgraph: ["@langchain/langgraph"],
33
+ "openai-agents-sdk": ["@openai/agents", "openai-agents"],
34
+ };
35
+ export async function detectTechnologyProfile(root, projectName) {
36
+ const evidence = new Map();
37
+ const warnings = [];
38
+ const files = await walkFiles(root, warnings);
39
+ const relativeFiles = files.map((file) => path.relative(root, file).split(path.sep).join("/"));
40
+ detectByExtensions(relativeFiles, evidence);
41
+ await detectNodeProjects(root, relativeFiles, evidence, warnings);
42
+ await detectManifestTechnologies(root, relativeFiles, evidence, warnings);
43
+ await detectInfrastructure(root, relativeFiles, evidence);
44
+ const technologies = [...evidence.entries()]
45
+ .filter(([, items]) => items.length > 0)
46
+ .sort(([left], [right]) => left.localeCompare(right))
47
+ .map(([slug, items]) => ({ slug, name: DISPLAY_NAMES[slug] ?? slug, evidence: dedupeEvidence(items) }));
48
+ return {
49
+ schema_version: "1",
50
+ project_name: projectName,
51
+ detected_at: new Date().toISOString(),
52
+ repository_root: ".",
53
+ technologies,
54
+ matched_packs: technologies.map((item) => item.slug),
55
+ warnings,
56
+ };
57
+ }
58
+ async function walkFiles(root, warnings) {
59
+ const files = [];
60
+ const pending = [root];
61
+ while (pending.length > 0 && files.length < MAX_FILES) {
62
+ const dir = pending.pop();
63
+ let entries;
64
+ try {
65
+ entries = await readdir(dir, { withFileTypes: true });
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ entries.sort((a, b) => a.name.localeCompare(b.name));
71
+ for (const entry of entries) {
72
+ if (entry.isSymbolicLink())
73
+ continue;
74
+ const fullPath = path.join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ if (!EXCLUDED_DIRS.has(entry.name))
77
+ pending.push(fullPath);
78
+ }
79
+ else if (entry.isFile()) {
80
+ files.push(fullPath);
81
+ if (files.length >= MAX_FILES)
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ if (files.length >= MAX_FILES)
87
+ warnings.push(`File scan stopped at the ${MAX_FILES.toLocaleString()} file safety limit.`);
88
+ return files;
89
+ }
90
+ function detectByExtensions(files, evidence) {
91
+ const extensionRules = {
92
+ ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", ".jsx": "javascript",
93
+ ".py": "python", ".go": "go", ".rs": "rust", ".java": "java",
94
+ };
95
+ const firstBySlug = new Map();
96
+ for (const file of files) {
97
+ const slug = extensionRules[path.extname(file).toLowerCase()];
98
+ if (slug && !firstBySlug.has(slug))
99
+ firstBySlug.set(slug, file);
100
+ }
101
+ for (const [slug, file] of firstBySlug)
102
+ addEvidence(evidence, slug, "extension", file, `Detected ${path.extname(file)} source files`);
103
+ }
104
+ async function detectNodeProjects(root, files, evidence, warnings) {
105
+ const manifests = files.filter((file) => path.basename(file) === "package.json");
106
+ for (const relativePath of manifests) {
107
+ const value = await readJsonSafe(path.join(root, relativePath), warnings);
108
+ if (!value || typeof value !== "object")
109
+ continue;
110
+ addEvidence(evidence, "nodejs", "manifest", relativePath, "Node.js package manifest");
111
+ const record = value;
112
+ const dependencyGroups = [record.dependencies, record.devDependencies, record.peerDependencies, record.optionalDependencies];
113
+ const names = new Set();
114
+ for (const group of dependencyGroups) {
115
+ if (group && typeof group === "object")
116
+ Object.keys(group).forEach((name) => names.add(name));
117
+ }
118
+ for (const [slug, dependencies] of Object.entries(DEPENDENCY_RULES)) {
119
+ for (const dependency of dependencies) {
120
+ if (names.has(dependency))
121
+ addEvidence(evidence, slug, "dependency", relativePath, dependency);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ async function detectManifestTechnologies(root, files, evidence, warnings) {
127
+ const exactRules = {
128
+ "pyproject.toml": "python", "requirements.txt": "python", "Pipfile": "python", "poetry.lock": "python",
129
+ "go.mod": "go", "Cargo.toml": "rust", "pom.xml": "java", "build.gradle": "java", "build.gradle.kts": "java",
130
+ };
131
+ for (const relativePath of files) {
132
+ const basename = path.basename(relativePath);
133
+ const slug = exactRules[basename];
134
+ if (slug)
135
+ addEvidence(evidence, slug, "manifest", relativePath, basename);
136
+ if (["pyproject.toml", "requirements.txt", "Pipfile"].includes(basename)) {
137
+ const content = await readSmallText(path.join(root, relativePath), warnings);
138
+ if (!content)
139
+ continue;
140
+ const lower = content.toLowerCase();
141
+ for (const [needle, target] of [["django", "django"], ["flask", "flask"], ["fastapi", "fastapi"], ["langchain", "langchain"], ["langgraph", "langgraph"]]) {
142
+ if (lower.includes(needle))
143
+ addEvidence(evidence, target, "dependency", relativePath, needle);
144
+ }
145
+ }
146
+ if (["pom.xml", "build.gradle", "build.gradle.kts"].includes(basename)) {
147
+ const content = await readSmallText(path.join(root, relativePath), warnings);
148
+ if (content?.includes("spring-boot"))
149
+ addEvidence(evidence, "java-spring-boot", "dependency", relativePath, "spring-boot");
150
+ }
151
+ }
152
+ }
153
+ async function detectInfrastructure(root, files, evidence) {
154
+ for (const relativePath of files) {
155
+ const basename = path.basename(relativePath).toLowerCase();
156
+ if (basename === "dockerfile" || basename.startsWith("docker-compose") || basename.startsWith("compose.")) {
157
+ addEvidence(evidence, "docker", "file", relativePath, "Container configuration");
158
+ }
159
+ if (relativePath.startsWith(".github/workflows/") && /\.ya?ml$/i.test(relativePath)) {
160
+ addEvidence(evidence, "github-actions", "file", relativePath, "GitHub Actions workflow");
161
+ }
162
+ if (/\.tf$/i.test(relativePath)) {
163
+ const content = (await readFile(path.join(root, relativePath), "utf8").catch(() => "")).slice(0, 200_000).toLowerCase();
164
+ if (content.includes("provider \"aws\"") || content.includes("hashicorp/aws"))
165
+ addEvidence(evidence, "terraform-aws", "content", relativePath, "AWS Terraform provider");
166
+ if (content.includes("provider \"azurerm\"") || content.includes("hashicorp/azurerm"))
167
+ addEvidence(evidence, "terraform-azure", "content", relativePath, "Azure Terraform provider");
168
+ if (content.includes("provider \"google\"") || content.includes("hashicorp/google"))
169
+ addEvidence(evidence, "terraform-gcp", "content", relativePath, "Google Cloud Terraform provider");
170
+ }
171
+ if (/\.(yaml|yml)$/i.test(relativePath) && /(k8s|kubernetes|helm|charts?)/i.test(relativePath)) {
172
+ addEvidence(evidence, "kubernetes", "file", relativePath, "Kubernetes or Helm configuration");
173
+ }
174
+ if (/openapi|swagger/i.test(basename) && /\.(json|ya?ml)$/i.test(basename)) {
175
+ addEvidence(evidence, "openapi-rest-api", "file", relativePath, "OpenAPI specification");
176
+ }
177
+ }
178
+ }
179
+ async function readJsonSafe(filePath, warnings) {
180
+ try {
181
+ const metadata = await stat(filePath);
182
+ if (metadata.size > MAX_MANIFEST_BYTES) {
183
+ warnings.push(`Skipped unusually large manifest: ${path.basename(filePath)}`);
184
+ return undefined;
185
+ }
186
+ return JSON.parse(await readFile(filePath, "utf8"));
187
+ }
188
+ catch {
189
+ warnings.push(`Could not parse ${path.basename(filePath)}.`);
190
+ return undefined;
191
+ }
192
+ }
193
+ async function readSmallText(filePath, warnings) {
194
+ try {
195
+ const metadata = await stat(filePath);
196
+ if (metadata.size > MAX_MANIFEST_BYTES)
197
+ return undefined;
198
+ return await readFile(filePath, "utf8");
199
+ }
200
+ catch {
201
+ warnings.push(`Could not read ${path.basename(filePath)}.`);
202
+ return undefined;
203
+ }
204
+ }
205
+ function addEvidence(map, slug, kind, filePath, detail) {
206
+ const current = map.get(slug) ?? [];
207
+ current.push({ kind, path: filePath, detail });
208
+ map.set(slug, current);
209
+ }
210
+ function dedupeEvidence(items) {
211
+ return [...new Map(items.map((item) => [`${item.kind}:${item.path}:${item.detail}`, item])).values()]
212
+ .sort((left, right) => left.path.localeCompare(right.path) || left.detail.localeCompare(right.detail));
213
+ }
214
+ //# sourceMappingURL=detector.js.map