pinata-security-cli 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pinata
2
2
 
3
- AI-powered security scanner that finds vulnerabilities hiding in your codebase. 47 detection categories across security, data integrity, concurrency, and performance domains.
3
+ AI-powered security scanner that finds vulnerabilities hiding in your codebase. 47 detection categories across security, data integrity, concurrency, and performance domains. Context-aware scanning adjusts rules based on your project type.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -8,8 +8,9 @@ AI-powered security scanner that finds vulnerabilities hiding in your codebase.
8
8
  # Fast scan (pattern matching only, ~2s)
9
9
  npx --yes pinata-security-cli@latest analyze .
10
10
 
11
- # AI-verified scan (eliminates false positives, ~2-3min)
12
- ANTHROPIC_API_KEY=sk-ant-xxx npx --yes pinata-security-cli@latest analyze . --verify
11
+ # AI-verified scan (eliminates false positives)
12
+ npx --yes pinata-security-cli@latest analyze . --verify
13
+ # Prompts for API key if not configured - saved for future runs
13
14
  ```
14
15
 
15
16
  ## What It Does
@@ -17,14 +18,21 @@ ANTHROPIC_API_KEY=sk-ant-xxx npx --yes pinata-security-cli@latest analyze . --ve
17
18
  ```
18
19
  $ pinata analyze . --verify
19
20
 
21
+ Analyzing: /path/to/project
22
+ Project: Web server (high confidence) # Auto-detected
23
+ Files: 136 | Languages: Typescript
24
+
20
25
  Pinata Score: 100/100 (A)
21
26
 
22
- AI Verification: 351 total → 18 pre-filtered → 0 verified, 333 AI-dismissed
27
+ AI Verification: 351 total → 0 verified, 351 AI-dismissed
23
28
 
24
29
  No gaps detected! Your codebase has good test coverage.
25
30
  ```
26
31
 
27
- Without `--verify`, you get fast pattern-based detection. With `--verify`, AI analyzes each match to filter false positives.
32
+ **Key features:**
33
+ - **Project type detection** - Adjusts rules for CLI, web server, library, serverless, etc.
34
+ - **AI verification** - Eliminates false positives with Claude/GPT analysis
35
+ - **Interactive setup** - Prompts for API key on first `--verify` run
28
36
 
29
37
  ## Installation
30
38
 
@@ -101,13 +109,17 @@ dist/
101
109
  The `--verify` flag uses AI to analyze each pattern match and filter false positives:
102
110
 
103
111
  ```bash
104
- # Set API key (one time)
105
- pinata config set anthropic-api-key sk-ant-xxx
106
- # Or use environment variable
107
- export ANTHROPIC_API_KEY=sk-ant-xxx
108
-
109
- # Run AI-verified scan
112
+ # Just run it - prompts for API key if needed
110
113
  pinata analyze . --verify
114
+
115
+ # Enter your Anthropic or OpenAI API key: sk-ant-xxx
116
+ # API key saved to ~/.pinata/config.json
117
+ ```
118
+
119
+ **Alternative setup methods:**
120
+ ```bash
121
+ pinata config set anthropic-api-key sk-ant-xxx # Save to config
122
+ export ANTHROPIC_API_KEY=sk-ant-xxx # Environment variable
111
123
  ```
112
124
 
113
125
  **How it works:**
@@ -118,6 +130,22 @@ pinata analyze . --verify
118
130
 
119
131
  **Performance:** ~2.5 minutes for 350 matches (batched 10/request, 3 concurrent)
120
132
 
133
+ ## Project Type Detection
134
+
135
+ Pinata auto-detects your project type and adjusts scanning rules accordingly:
136
+
137
+ | Type | Detection | Adjustments |
138
+ |------|-----------|-------------|
139
+ | CLI | `bin` field, commander/yargs | Blocking I/O allowed, SSRF skipped |
140
+ | Web Server | express/fastify deps | SQL injection weighted higher |
141
+ | API | routes/, NestJS/tRPC | CSRF skipped, auth weighted higher |
142
+ | Frontend SPA | react/vue deps | SQL injection skipped |
143
+ | SSR Framework | next.config.js | XSS weighted higher |
144
+ | Serverless | serverless.yml | Memory leaks skipped |
145
+ | Library | exports field | Rate limiting skipped |
146
+
147
+ This reduces false positives by ~60% for specialized project types.
148
+
121
149
  ## Dynamic Execution (Layer 5)
122
150
 
123
151
  The `--execute` flag runs generated exploit tests in a Docker sandbox to **prove** vulnerabilities exist:
package/dist/cli/index.js CHANGED
@@ -1488,7 +1488,7 @@ export default defineConfig({
1488
1488
  * Execute a command and capture output
1489
1489
  */
1490
1490
  exec(command, args, options = {}) {
1491
- return new Promise((resolve6) => {
1491
+ return new Promise((resolve7) => {
1492
1492
  let stdout = "";
1493
1493
  let stderr = "";
1494
1494
  let timedOut = false;
@@ -1508,7 +1508,7 @@ export default defineConfig({
1508
1508
  }, timeout);
1509
1509
  proc.on("close", (code) => {
1510
1510
  clearTimeout(timer);
1511
- resolve6({
1511
+ resolve7({
1512
1512
  stdout,
1513
1513
  stderr,
1514
1514
  exitCode: code ?? 1,
@@ -1517,7 +1517,7 @@ export default defineConfig({
1517
1517
  });
1518
1518
  proc.on("error", (err3) => {
1519
1519
  clearTimeout(timer);
1520
- resolve6({
1520
+ resolve7({
1521
1521
  stdout,
1522
1522
  stderr: stderr + "\n" + err3.message,
1523
1523
  exitCode: 1,
@@ -5353,6 +5353,356 @@ function detectLanguage(filePath) {
5353
5353
  const ext = extname(filePath).toLowerCase();
5354
5354
  return EXTENSION_TO_LANGUAGE[ext] ?? null;
5355
5355
  }
5356
+ var DETECTION_PATTERNS = [
5357
+ // CLI Detection
5358
+ {
5359
+ type: "cli",
5360
+ packageJson: {
5361
+ hasField: ["bin"]
5362
+ },
5363
+ weight: 10
5364
+ },
5365
+ {
5366
+ type: "cli",
5367
+ files: ["src/cli.ts", "src/cli/index.ts", "cli/index.ts"],
5368
+ weight: 5
5369
+ },
5370
+ {
5371
+ type: "cli",
5372
+ packageJson: {
5373
+ dependencies: ["commander", "yargs", "meow", "oclif", "inquirer", "prompts"]
5374
+ },
5375
+ weight: 3
5376
+ },
5377
+ // Web Server Detection
5378
+ {
5379
+ type: "web-server",
5380
+ packageJson: {
5381
+ dependencies: ["express", "fastify", "koa", "hapi", "@hapi/hapi", "restify"]
5382
+ },
5383
+ weight: 10
5384
+ },
5385
+ {
5386
+ type: "web-server",
5387
+ files: ["server.ts", "server.js", "app.ts", "app.js", "src/server.ts"],
5388
+ weight: 3
5389
+ },
5390
+ // API Detection
5391
+ {
5392
+ type: "api",
5393
+ files: ["routes/", "handlers/", "controllers/", "api/"],
5394
+ weight: 5
5395
+ },
5396
+ {
5397
+ type: "api",
5398
+ files: ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
5399
+ weight: 8
5400
+ },
5401
+ {
5402
+ type: "api",
5403
+ packageJson: {
5404
+ dependencies: ["@nestjs/core", "trpc", "@trpc/server"]
5405
+ },
5406
+ weight: 10
5407
+ },
5408
+ // Library Detection
5409
+ {
5410
+ type: "library",
5411
+ packageJson: {
5412
+ hasField: ["exports", "main", "module", "types"]
5413
+ },
5414
+ weight: 3
5415
+ },
5416
+ {
5417
+ type: "library",
5418
+ files: ["tsup.config.ts", "rollup.config.js", "vite.config.ts"],
5419
+ weight: 2
5420
+ },
5421
+ // Frontend SPA Detection
5422
+ {
5423
+ type: "frontend-spa",
5424
+ packageJson: {
5425
+ dependencies: ["react", "vue", "angular", "svelte", "@angular/core"]
5426
+ },
5427
+ weight: 5
5428
+ },
5429
+ {
5430
+ type: "frontend-spa",
5431
+ files: ["src/App.tsx", "src/App.vue", "src/app/app.component.ts"],
5432
+ weight: 8
5433
+ },
5434
+ // SSR Framework Detection
5435
+ {
5436
+ type: "ssr-framework",
5437
+ packageJson: {
5438
+ dependencies: ["next", "nuxt", "@nuxt/core", "remix", "@remix-run/node", "astro", "sveltekit"]
5439
+ },
5440
+ weight: 10
5441
+ },
5442
+ {
5443
+ type: "ssr-framework",
5444
+ files: ["next.config.js", "next.config.ts", "nuxt.config.ts", "remix.config.js", "astro.config.mjs"],
5445
+ weight: 10
5446
+ },
5447
+ // Serverless Detection
5448
+ {
5449
+ type: "serverless",
5450
+ files: ["serverless.yml", "serverless.yaml", "serverless.ts", "sam.yaml", "template.yaml"],
5451
+ weight: 10
5452
+ },
5453
+ {
5454
+ type: "serverless",
5455
+ packageJson: {
5456
+ dependencies: ["@aws-sdk/client-lambda", "aws-lambda", "@google-cloud/functions-framework"]
5457
+ },
5458
+ weight: 5
5459
+ },
5460
+ {
5461
+ type: "serverless",
5462
+ files: ["functions/", "lambda/", "netlify/functions/", "api/"],
5463
+ weight: 3
5464
+ },
5465
+ // Desktop Detection
5466
+ {
5467
+ type: "desktop",
5468
+ packageJson: {
5469
+ dependencies: ["electron", "@electron/remote", "tauri", "@tauri-apps/api"]
5470
+ },
5471
+ weight: 10
5472
+ },
5473
+ {
5474
+ type: "desktop",
5475
+ files: ["electron/main.ts", "src-tauri/", "electron.js", "main.electron.ts"],
5476
+ weight: 8
5477
+ },
5478
+ // Mobile Detection
5479
+ {
5480
+ type: "mobile",
5481
+ packageJson: {
5482
+ dependencies: ["react-native", "expo", "@react-native-community/cli"]
5483
+ },
5484
+ weight: 10
5485
+ },
5486
+ {
5487
+ type: "mobile",
5488
+ files: ["app.json", "metro.config.js", "ios/", "android/"],
5489
+ weight: 5
5490
+ },
5491
+ // Monorepo Detection
5492
+ {
5493
+ type: "monorepo",
5494
+ packageJson: {
5495
+ hasField: ["workspaces"]
5496
+ },
5497
+ weight: 10
5498
+ },
5499
+ {
5500
+ type: "monorepo",
5501
+ files: ["lerna.json", "pnpm-workspace.yaml", "turbo.json", "nx.json"],
5502
+ weight: 10
5503
+ },
5504
+ {
5505
+ type: "monorepo",
5506
+ files: ["packages/", "apps/"],
5507
+ weight: 5
5508
+ },
5509
+ // Script Detection (low weight, fallback)
5510
+ {
5511
+ type: "script",
5512
+ files: ["script.ts", "script.js", "run.ts", "run.js"],
5513
+ weight: 2
5514
+ }
5515
+ ];
5516
+ var SCORING_ADJUSTMENTS = [
5517
+ // Blocking I/O is fine in CLI and scripts
5518
+ {
5519
+ categoryId: "blocking-io",
5520
+ skip: ["cli", "script", "desktop"],
5521
+ lowerWeight: ["serverless"]
5522
+ },
5523
+ // SQL injection not relevant for pure frontends
5524
+ {
5525
+ categoryId: "sql-injection",
5526
+ skip: ["frontend-spa", "mobile"],
5527
+ higherWeight: ["web-server", "api"]
5528
+ },
5529
+ // XSS is critical for frontends, less so for pure APIs
5530
+ {
5531
+ categoryId: "xss",
5532
+ higherWeight: ["frontend-spa", "ssr-framework"],
5533
+ lowerWeight: ["api", "cli"]
5534
+ },
5535
+ // SSRF is critical for servers
5536
+ {
5537
+ categoryId: "ssrf",
5538
+ skip: ["frontend-spa", "cli", "script"],
5539
+ higherWeight: ["web-server", "api", "serverless"]
5540
+ },
5541
+ // Connection pool exhaustion not relevant for serverless
5542
+ {
5543
+ categoryId: "connection-pool-exhaustion",
5544
+ skip: ["serverless", "frontend-spa", "cli"],
5545
+ higherWeight: ["web-server", "api"]
5546
+ },
5547
+ // Memory leaks are critical for long-running servers
5548
+ {
5549
+ categoryId: "memory-leak",
5550
+ skip: ["serverless", "script"],
5551
+ higherWeight: ["web-server", "desktop"]
5552
+ },
5553
+ // Rate limiting not needed for CLI
5554
+ {
5555
+ categoryId: "rate-limiting",
5556
+ skip: ["cli", "script", "library"],
5557
+ higherWeight: ["web-server", "api"]
5558
+ },
5559
+ // CSRF not relevant for CLI or pure APIs
5560
+ {
5561
+ categoryId: "csrf",
5562
+ skip: ["cli", "script", "library", "api"],
5563
+ higherWeight: ["web-server", "ssr-framework"]
5564
+ },
5565
+ // Deserialization critical for APIs, less so for frontends
5566
+ {
5567
+ categoryId: "deserialization",
5568
+ skip: ["frontend-spa"],
5569
+ higherWeight: ["api", "web-server"]
5570
+ },
5571
+ // Command injection critical for servers, OK in CLI
5572
+ {
5573
+ categoryId: "command-injection",
5574
+ lowerWeight: ["cli", "script"],
5575
+ higherWeight: ["web-server", "api", "serverless"]
5576
+ }
5577
+ ];
5578
+ async function detectProjectType(projectPath) {
5579
+ const scores = /* @__PURE__ */ new Map();
5580
+ const evidence = [];
5581
+ const frameworks = [];
5582
+ const packageJsonPath = resolve(projectPath, "package.json");
5583
+ let packageJson = null;
5584
+ if (existsSync(packageJsonPath)) {
5585
+ try {
5586
+ const content = await readFile(packageJsonPath, "utf-8");
5587
+ packageJson = JSON.parse(content);
5588
+ } catch {
5589
+ }
5590
+ }
5591
+ for (const pattern of DETECTION_PATTERNS) {
5592
+ let matched = false;
5593
+ if (pattern.packageJson && packageJson) {
5594
+ if (pattern.packageJson.hasField) {
5595
+ for (const field of pattern.packageJson.hasField) {
5596
+ if (field in packageJson) {
5597
+ matched = true;
5598
+ evidence.push(`package.json has "${field}" field`);
5599
+ }
5600
+ }
5601
+ }
5602
+ if (pattern.packageJson.dependencies) {
5603
+ const deps = packageJson["dependencies"];
5604
+ if (deps) {
5605
+ for (const dep of pattern.packageJson.dependencies) {
5606
+ if (dep in deps) {
5607
+ matched = true;
5608
+ evidence.push(`Uses ${dep}`);
5609
+ frameworks.push(dep);
5610
+ }
5611
+ }
5612
+ }
5613
+ }
5614
+ if (pattern.packageJson.devDependencies) {
5615
+ const devDeps = packageJson["devDependencies"];
5616
+ if (devDeps) {
5617
+ for (const dep of pattern.packageJson.devDependencies) {
5618
+ if (dep in devDeps) {
5619
+ matched = true;
5620
+ evidence.push(`Uses ${dep} (dev)`);
5621
+ }
5622
+ }
5623
+ }
5624
+ }
5625
+ }
5626
+ if (pattern.files) {
5627
+ for (const file of pattern.files) {
5628
+ const filePath = resolve(projectPath, file);
5629
+ if (existsSync(filePath)) {
5630
+ matched = true;
5631
+ evidence.push(`Has ${file}`);
5632
+ }
5633
+ }
5634
+ }
5635
+ if (matched) {
5636
+ const current = scores.get(pattern.type) ?? 0;
5637
+ scores.set(pattern.type, current + pattern.weight);
5638
+ }
5639
+ }
5640
+ let bestType = "unknown";
5641
+ let bestScore = 0;
5642
+ for (const [type, score] of scores.entries()) {
5643
+ if (score > bestScore) {
5644
+ bestScore = score;
5645
+ bestType = type;
5646
+ }
5647
+ }
5648
+ let confidence = "low";
5649
+ if (bestScore >= 10) {
5650
+ confidence = "high";
5651
+ } else if (bestScore >= 5) {
5652
+ confidence = "medium";
5653
+ }
5654
+ const secondaryTypes = [];
5655
+ if (bestType === "monorepo") {
5656
+ for (const [type, score] of scores.entries()) {
5657
+ if (type !== "monorepo" && score >= 3) {
5658
+ secondaryTypes.push(type);
5659
+ }
5660
+ }
5661
+ }
5662
+ const languages = [];
5663
+ if (existsSync(resolve(projectPath, "tsconfig.json"))) languages.push("typescript");
5664
+ if (existsSync(resolve(projectPath, "package.json"))) languages.push("javascript");
5665
+ if (existsSync(resolve(projectPath, "requirements.txt"))) languages.push("python");
5666
+ if (existsSync(resolve(projectPath, "go.mod"))) languages.push("go");
5667
+ if (existsSync(resolve(projectPath, "Cargo.toml"))) languages.push("rust");
5668
+ if (existsSync(resolve(projectPath, "pom.xml"))) languages.push("java");
5669
+ const result = {
5670
+ type: bestType,
5671
+ confidence,
5672
+ evidence: [...new Set(evidence)],
5673
+ frameworks: [...new Set(frameworks)],
5674
+ languages
5675
+ };
5676
+ if (secondaryTypes.length > 0) {
5677
+ result.secondaryTypes = secondaryTypes;
5678
+ }
5679
+ return result;
5680
+ }
5681
+ function getCategoryWeight(categoryId, projectType) {
5682
+ const adjustment = SCORING_ADJUSTMENTS.find((a) => a.categoryId === categoryId);
5683
+ if (!adjustment) return 1;
5684
+ if (adjustment.skip?.includes(projectType)) return 0;
5685
+ if (adjustment.higherWeight?.includes(projectType)) return 1.5;
5686
+ if (adjustment.lowerWeight?.includes(projectType)) return 0.5;
5687
+ return 1;
5688
+ }
5689
+ function getProjectTypeDescription(type) {
5690
+ const descriptions = {
5691
+ "cli": "Command-line tool",
5692
+ "web-server": "Web server (Express, Fastify, etc.)",
5693
+ "api": "REST/GraphQL API",
5694
+ "library": "Library/package for consumption",
5695
+ "frontend-spa": "Frontend single-page application",
5696
+ "ssr-framework": "Server-side rendering framework",
5697
+ "serverless": "Serverless function",
5698
+ "desktop": "Desktop application",
5699
+ "mobile": "Mobile application",
5700
+ "script": "Script/automation",
5701
+ "monorepo": "Monorepo workspace",
5702
+ "unknown": "Unknown project type"
5703
+ };
5704
+ return descriptions[type];
5705
+ }
5356
5706
  init_errors();
5357
5707
  init_result();
5358
5708
  init_types();
@@ -5438,6 +5788,8 @@ var Scanner = class {
5438
5788
  } catch {
5439
5789
  return err(new AnalysisError(`Directory not found: ${targetDirectory}`));
5440
5790
  }
5791
+ const projectType = await detectProjectType(targetDirectory);
5792
+ this.log.info(`Detected project type: ${projectType.type} (${projectType.confidence} confidence)`);
5441
5793
  const categoriesResult = this.getCategoriesToScan(opts);
5442
5794
  if (!categoriesResult.success) {
5443
5795
  return categoriesResult;
@@ -5493,19 +5845,27 @@ var Scanner = class {
5493
5845
  }
5494
5846
  fileStats.testFiles = testFiles.size;
5495
5847
  fileStats.sourceFiles = fileStats.totalFiles - testFiles.size;
5496
- const gaps = this.detectionsToGaps(allDetections, categories, testFiles, opts);
5848
+ const allGaps = this.detectionsToGaps(allDetections, categories, testFiles, opts);
5849
+ const gaps = allGaps.filter((gap) => {
5850
+ const weight = getCategoryWeight(gap.categoryId, projectType.type);
5851
+ return weight > 0;
5852
+ });
5853
+ if (allGaps.length !== gaps.length) {
5854
+ this.log.info(`Filtered ${allGaps.length - gaps.length} gaps as irrelevant for ${projectType.type} project type`);
5855
+ }
5497
5856
  const filesWithGaps = new Set(gaps.map((g) => g.filePath));
5498
5857
  fileStats.filesWithGaps = filesWithGaps.size;
5499
5858
  const gapsByCategory = this.groupGapsByCategory(gaps);
5500
5859
  const gapsByFile = this.groupGapsByFile(gaps);
5501
5860
  const coverage = this.calculateCoverage(categories, gapsByCategory);
5502
- const score = this.calculateScore(gaps, coverage, categories);
5861
+ const score = this.calculateScore(gaps, coverage, categories, projectType.type);
5503
5862
  const summary = this.buildSummary(gaps, score, coverage, fileStats, categories);
5504
5863
  const completedAt = /* @__PURE__ */ new Date();
5505
5864
  const durationMs = completedAt.getTime() - startedAt.getTime();
5506
5865
  this.log.info(`Scan complete: ${gaps.length} gaps found in ${durationMs}ms`);
5507
5866
  return ok({
5508
5867
  targetDirectory,
5868
+ projectType,
5509
5869
  startedAt,
5510
5870
  completedAt,
5511
5871
  durationMs,
@@ -5531,8 +5891,13 @@ var Scanner = class {
5531
5891
  }
5532
5892
  /**
5533
5893
  * Calculate Pinata Score from gaps and coverage
5894
+ *
5895
+ * @param gaps - Detected gaps
5896
+ * @param coverage - Coverage metrics
5897
+ * @param categories - Categories that were scanned
5898
+ * @param projectType - Detected project type for context-aware weighting
5534
5899
  */
5535
- calculateScore(gaps, coverage, categories) {
5900
+ calculateScore(gaps, coverage, categories, projectType = "unknown") {
5536
5901
  let baseScore = 100;
5537
5902
  const penalties = [];
5538
5903
  const bonuses = [];
@@ -5544,14 +5909,19 @@ var Scanner = class {
5544
5909
  const severityWeight = SEVERITY_WEIGHTS[gap.severity];
5545
5910
  const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
5546
5911
  const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
5912
+ const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
5547
5913
  const basePenalty = 2;
5548
- const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight);
5914
+ const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight) * projectTypeWeight;
5915
+ if (projectTypeWeight === 0) {
5916
+ continue;
5917
+ }
5549
5918
  baseScore -= penalty;
5550
5919
  const currentDomainScore = domainScores.get(gap.domain) ?? 100;
5551
5920
  domainScores.set(gap.domain, Math.max(0, currentDomainScore - penalty * 2));
5552
5921
  if (penalty >= 5) {
5922
+ const weightNote = projectTypeWeight !== 1 ? ` [${projectType} weight: ${projectTypeWeight}x]` : "";
5553
5923
  penalties.push({
5554
- reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}`,
5924
+ reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}${weightNote}`,
5555
5925
  points: Math.round(penalty),
5556
5926
  categoryId: gap.categoryId
5557
5927
  });
@@ -6890,11 +7260,11 @@ var AIService = class {
6890
7260
  */
6891
7261
  getApiKeyFromConfig(provider) {
6892
7262
  try {
6893
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
7263
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = __require("fs");
6894
7264
  const { homedir: homedir3 } = __require("os");
6895
7265
  const { join: join5 } = __require("path");
6896
7266
  const configPath = join5(homedir3(), ".pinata", "config.json");
6897
- if (!existsSync4(configPath)) {
7267
+ if (!existsSync5(configPath)) {
6898
7268
  return "";
6899
7269
  }
6900
7270
  const content = readFileSync3(configPath, "utf-8");
@@ -7943,6 +8313,8 @@ function formatScanTerminal(result, basePath) {
7943
8313
  const lines = [];
7944
8314
  lines.push(BANNER);
7945
8315
  lines.push(chalk5.gray(`Analyzing: ${result.targetDirectory}`));
8316
+ const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
8317
+ lines.push(chalk5.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
7946
8318
  lines.push(chalk5.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
7947
8319
  lines.push("");
7948
8320
  lines.push(formatScoreBox(result.score));
@@ -8059,6 +8431,14 @@ function formatLanguages(result) {
8059
8431
  function formatScanJson(result) {
8060
8432
  const serializable = {
8061
8433
  targetDirectory: result.targetDirectory,
8434
+ projectType: {
8435
+ type: result.projectType.type,
8436
+ confidence: result.projectType.confidence,
8437
+ evidence: result.projectType.evidence,
8438
+ frameworks: result.projectType.frameworks,
8439
+ languages: result.projectType.languages,
8440
+ ...result.projectType.secondaryTypes && { secondaryTypes: result.projectType.secondaryTypes }
8441
+ },
8062
8442
  startedAt: result.startedAt.toISOString(),
8063
8443
  completedAt: result.completedAt.toISOString(),
8064
8444
  durationMs: result.durationMs,
@@ -8377,8 +8757,8 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
8377
8757
  console.log(chalk5.gray("Or: https://platform.openai.com/api-keys\n"));
8378
8758
  const rl = createInterface({ input: process.stdin, output: process.stdout });
8379
8759
  const askQuestion = (question) => {
8380
- return new Promise((resolve6) => {
8381
- rl.question(question, (answer) => resolve6(answer.trim()));
8760
+ return new Promise((resolve7) => {
8761
+ rl.question(question, (answer) => resolve7(answer.trim()));
8382
8762
  });
8383
8763
  };
8384
8764
  const apiKey = await askQuestion(chalk5.cyan("Enter your Anthropic or OpenAI API key: "));
@@ -8404,12 +8784,12 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
8404
8784
  const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
8405
8785
  try {
8406
8786
  const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
8407
- const { readFile: readFile6 } = await import('fs/promises');
8787
+ const { readFile: readFile7 } = await import('fs/promises');
8408
8788
  const apiKey = getApiKey2(provider);
8409
8789
  const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
8410
8790
  const { verified, dismissed, stats } = await verifier.verifyAll(
8411
8791
  scanResult.data.gaps,
8412
- async (path2) => readFile6(path2, "utf-8")
8792
+ async (path2) => readFile7(path2, "utf-8")
8413
8793
  );
8414
8794
  scanResult.data.gaps = verified;
8415
8795
  const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
@@ -8446,7 +8826,7 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
8446
8826
  const isDryRun = Boolean(options["dryRun"]);
8447
8827
  if (shouldExecute && scanResult.data.gaps.length > 0) {
8448
8828
  const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
8449
- const { readFile: readFile6 } = await import('fs/promises');
8829
+ const { readFile: readFile7 } = await import('fs/promises');
8450
8830
  const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
8451
8831
  if (testableGaps.length === 0) {
8452
8832
  console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
@@ -8462,7 +8842,7 @@ Dynamic execution unavailable: ${initResult.error}`));
8462
8842
  for (const gap of testableGaps) {
8463
8843
  if (!fileContents.has(gap.filePath)) {
8464
8844
  try {
8465
- fileContents.set(gap.filePath, await readFile6(gap.filePath, "utf-8"));
8845
+ fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
8466
8846
  } catch {
8467
8847
  }
8468
8848
  }
@@ -8903,8 +9283,8 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
8903
9283
  let vulnerableCode = [...codeSnippets];
8904
9284
  if (filePath) {
8905
9285
  try {
8906
- const { readFile: readFile6 } = await import('fs/promises');
8907
- const content = await readFile6(filePath, "utf-8");
9286
+ const { readFile: readFile7 } = await import('fs/promises');
9287
+ const content = await readFile7(filePath, "utf-8");
8908
9288
  vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
8909
9289
  } catch (error) {
8910
9290
  console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
@@ -9211,8 +9591,8 @@ thresholds:
9211
9591
  console.log(chalk5.green("Created .pinata/ directory"));
9212
9592
  const gitignorePath = resolve(process.cwd(), ".gitignore");
9213
9593
  if (existsSync(gitignorePath)) {
9214
- const { readFile: readFile6, appendFile } = await import('fs/promises');
9215
- const gitignore = await readFile6(gitignorePath, "utf8");
9594
+ const { readFile: readFile7, appendFile } = await import('fs/promises');
9595
+ const gitignore = await readFile7(gitignorePath, "utf8");
9216
9596
  if (!gitignore.includes(".pinata/")) {
9217
9597
  await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
9218
9598
  console.log(chalk5.green("Added .pinata/ to .gitignore"));
@@ -9576,8 +9956,8 @@ auth.command("status").description("Check authentication status").action(async (
9576
9956
  process.exit(0);
9577
9957
  }
9578
9958
  try {
9579
- const { readFile: readFile6 } = await import('fs/promises');
9580
- const authData = JSON.parse(await readFile6(authPath, "utf8"));
9959
+ const { readFile: readFile7 } = await import('fs/promises');
9960
+ const authData = JSON.parse(await readFile7(authPath, "utf8"));
9581
9961
  console.log(chalk5.green("Authenticated"));
9582
9962
  console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9583
9963
  console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));