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 +39 -11
- package/dist/cli/index.js +402 -22
- package/dist/cli/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
12
|
-
|
|
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 →
|
|
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
|
-
|
|
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
|
-
#
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 (!
|
|
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((
|
|
8381
|
-
rl.question(question, (answer) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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
|
|
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:
|
|
8907
|
-
const content = await
|
|
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:
|
|
9215
|
-
const gitignore = await
|
|
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:
|
|
9580
|
-
const authData = JSON.parse(await
|
|
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"}`));
|