@rahul-sch/vibeguard 1.0.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.
- package/README.md +162 -0
- package/bin/vibeguard.js +2 -0
- package/dist/ai/cache.d.ts +5 -0
- package/dist/ai/cache.js +20 -0
- package/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.js +71 -0
- package/dist/ai/prompts.d.ts +7 -0
- package/dist/ai/prompts.js +65 -0
- package/dist/ai/provider.d.ts +12 -0
- package/dist/ai/provider.js +93 -0
- package/dist/ai/types.d.ts +21 -0
- package/dist/ai/types.js +1 -0
- package/dist/cli/commands/fix.d.ts +7 -0
- package/dist/cli/commands/fix.js +140 -0
- package/dist/cli/commands/github.d.ts +6 -0
- package/dist/cli/commands/github.js +24 -0
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.js +54 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/options.d.ts +17 -0
- package/dist/cli/options.js +27 -0
- package/dist/config/defaults.d.ts +17 -0
- package/dist/config/defaults.js +21 -0
- package/dist/config/index.d.ts +17 -0
- package/dist/config/index.js +119 -0
- package/dist/config/schema.d.ts +20 -0
- package/dist/config/schema.js +39 -0
- package/dist/engine/file-walker.d.ts +12 -0
- package/dist/engine/file-walker.js +61 -0
- package/dist/engine/filter.d.ts +3 -0
- package/dist/engine/filter.js +50 -0
- package/dist/engine/index.d.ts +10 -0
- package/dist/engine/index.js +54 -0
- package/dist/engine/matcher.d.ts +10 -0
- package/dist/engine/matcher.js +47 -0
- package/dist/fix/engine.d.ts +37 -0
- package/dist/fix/engine.js +121 -0
- package/dist/fix/index.d.ts +2 -0
- package/dist/fix/index.js +2 -0
- package/dist/fix/patch.d.ts +23 -0
- package/dist/fix/patch.js +94 -0
- package/dist/fix/strategies.d.ts +21 -0
- package/dist/fix/strategies.js +213 -0
- package/dist/fix/types.d.ts +48 -0
- package/dist/fix/types.js +1 -0
- package/dist/github/client.d.ts +10 -0
- package/dist/github/client.js +43 -0
- package/dist/github/comment-formatter.d.ts +3 -0
- package/dist/github/comment-formatter.js +65 -0
- package/dist/github/index.d.ts +5 -0
- package/dist/github/index.js +5 -0
- package/dist/github/installer.d.ts +2 -0
- package/dist/github/installer.js +41 -0
- package/dist/github/types.d.ts +40 -0
- package/dist/github/types.js +1 -0
- package/dist/github/workflow-generator.d.ts +2 -0
- package/dist/github/workflow-generator.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/reporters/console.d.ts +9 -0
- package/dist/reporters/console.js +76 -0
- package/dist/reporters/index.d.ts +6 -0
- package/dist/reporters/index.js +17 -0
- package/dist/reporters/json.d.ts +5 -0
- package/dist/reporters/json.js +32 -0
- package/dist/reporters/sarif.d.ts +9 -0
- package/dist/reporters/sarif.js +78 -0
- package/dist/reporters/types.d.ts +5 -0
- package/dist/reporters/types.js +1 -0
- package/dist/rules/config.d.ts +2 -0
- package/dist/rules/config.js +31 -0
- package/dist/rules/dependencies.d.ts +2 -0
- package/dist/rules/dependencies.js +32 -0
- package/dist/rules/docker.d.ts +2 -0
- package/dist/rules/docker.js +44 -0
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.js +25 -0
- package/dist/rules/kubernetes.d.ts +2 -0
- package/dist/rules/kubernetes.js +44 -0
- package/dist/rules/node.d.ts +2 -0
- package/dist/rules/node.js +72 -0
- package/dist/rules/python.d.ts +2 -0
- package/dist/rules/python.js +91 -0
- package/dist/rules/secrets.d.ts +2 -0
- package/dist/rules/secrets.js +82 -0
- package/dist/rules/types.d.ts +75 -0
- package/dist/rules/types.js +1 -0
- package/dist/utils/binary-check.d.ts +1 -0
- package/dist/utils/binary-check.js +10 -0
- package/dist/utils/line-mapper.d.ts +6 -0
- package/dist/utils/line-mapper.js +40 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# VibeGuard
|
|
2
|
+
|
|
3
|
+
Regex-first security scanner for AI-generated ("vibe-coded") projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **23 detection rules** covering secrets, injection, Docker, Kubernetes, Python, Node.js
|
|
8
|
+
- **Fast regex scanning** - no AST parsing, works on any codebase size
|
|
9
|
+
- **Multiple output formats** - console (colored), JSON, SARIF (GitHub Code Scanning)
|
|
10
|
+
- **Optional AI verification** - reduce false positives with LLM verification (BYOK)
|
|
11
|
+
- **Zero config** - works out of the box with sensible defaults
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g vibeguard
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Scan current directory
|
|
23
|
+
vibeguard
|
|
24
|
+
|
|
25
|
+
# Scan specific path
|
|
26
|
+
vibeguard ./src
|
|
27
|
+
|
|
28
|
+
# Show only critical issues
|
|
29
|
+
vibeguard --severity critical
|
|
30
|
+
|
|
31
|
+
# Output as JSON
|
|
32
|
+
vibeguard --json
|
|
33
|
+
|
|
34
|
+
# Output as SARIF (for GitHub Code Scanning)
|
|
35
|
+
vibeguard --sarif > report.sarif
|
|
36
|
+
|
|
37
|
+
# Enable AI verification
|
|
38
|
+
vibeguard --ai --ai-key sk-xxx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Options
|
|
42
|
+
|
|
43
|
+
| Option | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `-s, --severity <level>` | Minimum severity: `critical`, `warning`, `info` (default: `warning`) |
|
|
46
|
+
| `-f, --format <type>` | Output format: `console`, `json`, `sarif` (default: `console`) |
|
|
47
|
+
| `--json` | Shorthand for `--format json` |
|
|
48
|
+
| `--sarif` | Shorthand for `--format sarif` |
|
|
49
|
+
| `-i, --ignore <pattern>` | Additional ignore patterns (can repeat) |
|
|
50
|
+
| `--max-file-size <bytes>` | Skip files larger than this (default: 1MB) |
|
|
51
|
+
| `--no-color` | Disable colored output |
|
|
52
|
+
| `-v, --verbose` | Show debug information |
|
|
53
|
+
| `--ai` | Enable AI verification |
|
|
54
|
+
| `--ai-key <key>` | API key for AI provider |
|
|
55
|
+
| `--ai-provider <name>` | AI provider: `openai`, `anthropic`, `groq` |
|
|
56
|
+
| `-c, --config <path>` | Path to config file |
|
|
57
|
+
|
|
58
|
+
## Exit Codes
|
|
59
|
+
|
|
60
|
+
| Code | Meaning |
|
|
61
|
+
|------|---------|
|
|
62
|
+
| 0 | No issues found (or only info-level) |
|
|
63
|
+
| 1 | Warning-level issues found |
|
|
64
|
+
| 2 | Critical-level issues found |
|
|
65
|
+
|
|
66
|
+
## Detection Rules
|
|
67
|
+
|
|
68
|
+
### Secrets & Injection
|
|
69
|
+
- `VG-SEC-001` - Dynamic code execution (eval)
|
|
70
|
+
- `VG-SEC-002` - SQL string concatenation
|
|
71
|
+
- `VG-SEC-003` - Hardcoded secrets/credentials
|
|
72
|
+
- `VG-SEC-004` - Secret logged to console
|
|
73
|
+
- `VG-SEC-005` - Secret in API response
|
|
74
|
+
|
|
75
|
+
### Python
|
|
76
|
+
- `VG-PY-001` - Shell command exec (shell=True)
|
|
77
|
+
- `VG-PY-002` - OS system call
|
|
78
|
+
- `VG-PY-003` - Unsafe YAML load
|
|
79
|
+
- `VG-PY-004` - Insecure pickle deserialization
|
|
80
|
+
- `VG-PY-005` - Flask debug mode enabled
|
|
81
|
+
- `VG-PY-006` - Disabled SSL verification
|
|
82
|
+
|
|
83
|
+
### Node.js
|
|
84
|
+
- `VG-NODE-001` - Child process exec
|
|
85
|
+
- `VG-NODE-002` - Spawn with shell
|
|
86
|
+
- `VG-NODE-003` - Unsafe HTML rendering (React)
|
|
87
|
+
- `VG-NODE-004` - Disabled TLS verification
|
|
88
|
+
- `VG-NODE-005` - TLS reject env bypass
|
|
89
|
+
|
|
90
|
+
### Docker
|
|
91
|
+
- `VG-DOCK-001` - Container running as root
|
|
92
|
+
- `VG-DOCK-002` - Docker socket exposed
|
|
93
|
+
- `VG-DOCK-003` - Privileged container
|
|
94
|
+
|
|
95
|
+
### Kubernetes
|
|
96
|
+
- `VG-K8S-001` - Cluster-admin role binding
|
|
97
|
+
- `VG-K8S-002` - Open ingress rules (0.0.0.0/0)
|
|
98
|
+
- `VG-K8S-003` - Missing runAsNonRoot
|
|
99
|
+
|
|
100
|
+
### Configuration
|
|
101
|
+
- `VG-CFG-001` - Service bound to 0.0.0.0
|
|
102
|
+
- `VG-CFG-002` - Public S3 bucket ACL
|
|
103
|
+
|
|
104
|
+
## GitHub Actions
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
name: Security Scan
|
|
108
|
+
|
|
109
|
+
on: [push, pull_request]
|
|
110
|
+
|
|
111
|
+
jobs:
|
|
112
|
+
vibeguard:
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
|
|
117
|
+
- name: Setup Node.js
|
|
118
|
+
uses: actions/setup-node@v4
|
|
119
|
+
with:
|
|
120
|
+
node-version: '20'
|
|
121
|
+
|
|
122
|
+
- name: Install VibeGuard
|
|
123
|
+
run: npm install -g vibeguard
|
|
124
|
+
|
|
125
|
+
- name: Run security scan
|
|
126
|
+
run: vibeguard --sarif > results.sarif
|
|
127
|
+
|
|
128
|
+
- name: Upload SARIF
|
|
129
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
130
|
+
with:
|
|
131
|
+
sarif_file: results.sarif
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration File
|
|
135
|
+
|
|
136
|
+
Create `vibeguard.config.json` in your project root:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"severity": "warning",
|
|
141
|
+
"ignore": ["**/test/**", "**/fixtures/**"],
|
|
142
|
+
"format": "console"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## AI Verification
|
|
147
|
+
|
|
148
|
+
VibeGuard supports optional AI verification to reduce false positives. Set your API key:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
export VIBEGUARD_AI_KEY=sk-xxx
|
|
152
|
+
vibeguard --ai
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Supported providers:
|
|
156
|
+
- OpenAI (default)
|
|
157
|
+
- Anthropic (`sk-ant-*` keys auto-detected)
|
|
158
|
+
- Groq (`gsk_*` keys auto-detected)
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/bin/vibeguard.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VerifyResponse } from './types.js';
|
|
2
|
+
export declare function getCacheKey(ruleId: string, snippet: string): string;
|
|
3
|
+
export declare function getCached(key: string): VerifyResponse | undefined;
|
|
4
|
+
export declare function setCache(key: string, response: VerifyResponse): void;
|
|
5
|
+
export declare function clearCache(): void;
|
package/dist/ai/cache.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const cache = new LRUCache({
|
|
4
|
+
max: 1000,
|
|
5
|
+
ttl: 1000 * 60 * 60 * 24, // 24h TTL
|
|
6
|
+
});
|
|
7
|
+
export function getCacheKey(ruleId, snippet) {
|
|
8
|
+
return createHash('sha256')
|
|
9
|
+
.update(ruleId + snippet)
|
|
10
|
+
.digest('hex');
|
|
11
|
+
}
|
|
12
|
+
export function getCached(key) {
|
|
13
|
+
return cache.get(key);
|
|
14
|
+
}
|
|
15
|
+
export function setCache(key, response) {
|
|
16
|
+
cache.set(key, response);
|
|
17
|
+
}
|
|
18
|
+
export function clearCache() {
|
|
19
|
+
cache.clear();
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Finding, DetectionRule } from '../rules/types.js';
|
|
2
|
+
import type { AIConfig, VerifyResponse } from './types.js';
|
|
3
|
+
export interface AIVerifier {
|
|
4
|
+
verify(finding: Finding, rule: DetectionRule): Promise<VerifyResponse>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createAIVerifier(config: AIConfig): AIVerifier;
|
|
7
|
+
export declare function verifyFindings(findings: Finding[], rules: Map<string, DetectionRule>, config: AIConfig, verbose?: boolean): Promise<Finding[]>;
|
|
8
|
+
export declare function detectProvider(apiKey: string): string;
|
|
9
|
+
export { type AIConfig, type VerifyRequest, type VerifyResponse } from './types.js';
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createProvider } from './provider.js';
|
|
2
|
+
import { getCacheKey, getCached, setCache } from './cache.js';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
export function createAIVerifier(config) {
|
|
5
|
+
const provider = createProvider(config);
|
|
6
|
+
return {
|
|
7
|
+
async verify(finding, rule) {
|
|
8
|
+
// Check cache first
|
|
9
|
+
const cacheKey = getCacheKey(finding.ruleId, finding.snippet);
|
|
10
|
+
const cached = getCached(cacheKey);
|
|
11
|
+
if (cached) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
// Build request with minimal context
|
|
15
|
+
const request = {
|
|
16
|
+
snippet: truncate(finding.snippet, 500),
|
|
17
|
+
ruleId: rule.id,
|
|
18
|
+
ruleTitle: rule.title,
|
|
19
|
+
ruleDescription: rule.message,
|
|
20
|
+
fileContext: `File: ${basename(finding.file)}, Language: ${rule.languages[0] || 'unknown'}`,
|
|
21
|
+
};
|
|
22
|
+
const response = await provider.verify(request);
|
|
23
|
+
// Cache the result
|
|
24
|
+
setCache(cacheKey, response);
|
|
25
|
+
return response;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function truncate(str, maxLen) {
|
|
30
|
+
if (str.length <= maxLen)
|
|
31
|
+
return str;
|
|
32
|
+
return str.slice(0, maxLen - 3) + '...';
|
|
33
|
+
}
|
|
34
|
+
export async function verifyFindings(findings, rules, config, verbose = false) {
|
|
35
|
+
const verifier = createAIVerifier(config);
|
|
36
|
+
const verified = [];
|
|
37
|
+
for (const finding of findings) {
|
|
38
|
+
const rule = rules.get(finding.ruleId);
|
|
39
|
+
if (!rule) {
|
|
40
|
+
verified.push(finding);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Only verify rules that have AI verification enabled
|
|
44
|
+
if (!rule.aiVerification?.enabled) {
|
|
45
|
+
verified.push(finding);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.error(`AI verifying: ${finding.ruleId} at ${finding.file}:${finding.line}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const response = await verifier.verify(finding, rule);
|
|
53
|
+
verified.push({
|
|
54
|
+
...finding,
|
|
55
|
+
aiVerdict: response,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// On error, keep finding without AI verdict
|
|
60
|
+
verified.push(finding);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return verified;
|
|
64
|
+
}
|
|
65
|
+
export function detectProvider(apiKey) {
|
|
66
|
+
if (apiKey.startsWith('sk-ant-'))
|
|
67
|
+
return 'anthropic';
|
|
68
|
+
if (apiKey.startsWith('gsk_'))
|
|
69
|
+
return 'groq';
|
|
70
|
+
return 'openai';
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VerifyRequest } from './types.js';
|
|
2
|
+
export declare function buildVerificationPrompt(request: VerifyRequest): string;
|
|
3
|
+
export declare function parseVerificationResponse(text: string): {
|
|
4
|
+
verdict: 'true_positive' | 'false_positive' | 'unsure';
|
|
5
|
+
confidence: number;
|
|
6
|
+
rationale: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function buildVerificationPrompt(request) {
|
|
2
|
+
return `You are a security code reviewer. Analyze the following code snippet for a potential security issue.
|
|
3
|
+
|
|
4
|
+
Rule: ${request.ruleId} - ${request.ruleTitle}
|
|
5
|
+
Issue: ${request.ruleDescription}
|
|
6
|
+
File: ${request.fileContext}
|
|
7
|
+
|
|
8
|
+
Code snippet:
|
|
9
|
+
\`\`\`
|
|
10
|
+
${request.snippet}
|
|
11
|
+
\`\`\`
|
|
12
|
+
|
|
13
|
+
Determine if this is a TRUE security issue or a FALSE POSITIVE.
|
|
14
|
+
|
|
15
|
+
Consider:
|
|
16
|
+
- Is this test/example code that won't run in production?
|
|
17
|
+
- Is the flagged pattern used safely with proper sanitization?
|
|
18
|
+
- Is there context that makes this safe (e.g., constants, trusted data)?
|
|
19
|
+
- Could this realistically be exploited?
|
|
20
|
+
|
|
21
|
+
Respond in JSON format:
|
|
22
|
+
{
|
|
23
|
+
"verdict": "true_positive" | "false_positive" | "unsure",
|
|
24
|
+
"confidence": 0.0-1.0,
|
|
25
|
+
"rationale": "Brief explanation"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
JSON response:`;
|
|
29
|
+
}
|
|
30
|
+
export function parseVerificationResponse(text) {
|
|
31
|
+
// Try to extract JSON from response
|
|
32
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
33
|
+
if (!jsonMatch) {
|
|
34
|
+
return {
|
|
35
|
+
verdict: 'unsure',
|
|
36
|
+
confidence: 0.5,
|
|
37
|
+
rationale: 'Failed to parse AI response',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
42
|
+
// Validate verdict
|
|
43
|
+
const validVerdicts = ['true_positive', 'false_positive', 'unsure'];
|
|
44
|
+
const verdict = validVerdicts.includes(parsed.verdict)
|
|
45
|
+
? parsed.verdict
|
|
46
|
+
: 'unsure';
|
|
47
|
+
// Validate confidence
|
|
48
|
+
let confidence = parseFloat(parsed.confidence);
|
|
49
|
+
if (isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
50
|
+
confidence = 0.5;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
verdict,
|
|
54
|
+
confidence,
|
|
55
|
+
rationale: String(parsed.rationale || 'No rationale provided'),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {
|
|
60
|
+
verdict: 'unsure',
|
|
61
|
+
confidence: 0.5,
|
|
62
|
+
rationale: 'Failed to parse AI response JSON',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AIProvider, AIConfig, VerifyRequest, VerifyResponse } from './types.js';
|
|
2
|
+
export declare class OpenAICompatibleProvider implements AIProvider {
|
|
3
|
+
name: string;
|
|
4
|
+
private apiKey;
|
|
5
|
+
private model;
|
|
6
|
+
private baseUrl;
|
|
7
|
+
constructor(config: AIConfig);
|
|
8
|
+
verify(request: VerifyRequest): Promise<VerifyResponse>;
|
|
9
|
+
private callOpenAI;
|
|
10
|
+
private callAnthropic;
|
|
11
|
+
}
|
|
12
|
+
export declare function createProvider(config: AIConfig): AIProvider;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { buildVerificationPrompt, parseVerificationResponse } from './prompts.js';
|
|
2
|
+
export class OpenAICompatibleProvider {
|
|
3
|
+
name;
|
|
4
|
+
apiKey;
|
|
5
|
+
model;
|
|
6
|
+
baseUrl;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.apiKey = config.apiKey;
|
|
9
|
+
this.name = config.provider;
|
|
10
|
+
// Set defaults based on provider
|
|
11
|
+
switch (config.provider) {
|
|
12
|
+
case 'anthropic':
|
|
13
|
+
this.baseUrl = 'https://api.anthropic.com/v1/messages';
|
|
14
|
+
this.model = config.model || 'claude-3-haiku-20240307';
|
|
15
|
+
break;
|
|
16
|
+
case 'groq':
|
|
17
|
+
this.baseUrl = 'https://api.groq.com/openai/v1/chat/completions';
|
|
18
|
+
this.model = config.model || 'llama-3.1-8b-instant';
|
|
19
|
+
break;
|
|
20
|
+
case 'openai':
|
|
21
|
+
default:
|
|
22
|
+
this.baseUrl = 'https://api.openai.com/v1/chat/completions';
|
|
23
|
+
this.model = config.model || 'gpt-4o-mini';
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async verify(request) {
|
|
28
|
+
const prompt = buildVerificationPrompt(request);
|
|
29
|
+
try {
|
|
30
|
+
let responseText;
|
|
31
|
+
if (this.name === 'anthropic') {
|
|
32
|
+
responseText = await this.callAnthropic(prompt);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
responseText = await this.callOpenAI(prompt);
|
|
36
|
+
}
|
|
37
|
+
return parseVerificationResponse(responseText);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
41
|
+
return {
|
|
42
|
+
verdict: 'unsure',
|
|
43
|
+
confidence: 0,
|
|
44
|
+
rationale: `AI verification failed: ${message}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async callOpenAI(prompt) {
|
|
49
|
+
const response = await fetch(this.baseUrl, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model: this.model,
|
|
57
|
+
messages: [{ role: 'user', content: prompt }],
|
|
58
|
+
max_tokens: 500,
|
|
59
|
+
temperature: 0.1,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
65
|
+
}
|
|
66
|
+
const data = (await response.json());
|
|
67
|
+
return data.choices[0]?.message?.content || '';
|
|
68
|
+
}
|
|
69
|
+
async callAnthropic(prompt) {
|
|
70
|
+
const response = await fetch(this.baseUrl, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
'x-api-key': this.apiKey,
|
|
75
|
+
'anthropic-version': '2023-06-01',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model: this.model,
|
|
79
|
+
max_tokens: 500,
|
|
80
|
+
messages: [{ role: 'user', content: prompt }],
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
86
|
+
}
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
return data.content[0]?.text || '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function createProvider(config) {
|
|
92
|
+
return new OpenAICompatibleProvider(config);
|
|
93
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface VerifyRequest {
|
|
2
|
+
snippet: string;
|
|
3
|
+
ruleId: string;
|
|
4
|
+
ruleTitle: string;
|
|
5
|
+
ruleDescription: string;
|
|
6
|
+
fileContext: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifyResponse {
|
|
9
|
+
verdict: 'true_positive' | 'false_positive' | 'unsure';
|
|
10
|
+
confidence: number;
|
|
11
|
+
rationale: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AIProvider {
|
|
14
|
+
name: string;
|
|
15
|
+
verify(request: VerifyRequest): Promise<VerifyResponse>;
|
|
16
|
+
}
|
|
17
|
+
export interface AIConfig {
|
|
18
|
+
provider: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
package/dist/ai/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CLIOptions } from '../../config/index.js';
|
|
2
|
+
export interface FixCommandOptions extends CLIOptions {
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
yes?: boolean;
|
|
5
|
+
git?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function fixCommand(targetPath: string | undefined, options: FixCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { scan } from '../../engine/index.js';
|
|
5
|
+
import { resolveConfig } from '../../config/index.js';
|
|
6
|
+
import { getFixableFindings, generateFix, printDiff, applyFixes, } from '../../fix/index.js';
|
|
7
|
+
async function promptConfirm(message) {
|
|
8
|
+
const rl = createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stderr,
|
|
11
|
+
});
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function fixCommand(targetPath = '.', options) {
|
|
20
|
+
const resolvedPath = resolve(targetPath);
|
|
21
|
+
const config = resolveConfig(resolvedPath, options);
|
|
22
|
+
if (config.verbose) {
|
|
23
|
+
console.error(`VibeGuard fix: ${config.targetPath}`);
|
|
24
|
+
}
|
|
25
|
+
// First, scan to find issues
|
|
26
|
+
const result = await scan(config);
|
|
27
|
+
if (result.findings.length === 0) {
|
|
28
|
+
console.log('No issues found.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Filter to fixable findings
|
|
32
|
+
const fixable = getFixableFindings(result.findings);
|
|
33
|
+
if (fixable.length === 0) {
|
|
34
|
+
console.log(`Found ${result.findings.length} issues, but none are auto-fixable.`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`Found ${result.findings.length} issues, ${fixable.length} are auto-fixable:\n`);
|
|
38
|
+
// Generate fixes
|
|
39
|
+
const fixResults = [];
|
|
40
|
+
const validFixes = [];
|
|
41
|
+
for (const finding of fixable) {
|
|
42
|
+
const fixResult = generateFix(finding, resolvedPath);
|
|
43
|
+
fixResults.push({ finding, result: fixResult });
|
|
44
|
+
if (fixResult.success && fixResult.fix) {
|
|
45
|
+
validFixes.push(fixResult.fix);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Show what we found
|
|
49
|
+
for (const { finding, result: fixResult } of fixResults) {
|
|
50
|
+
const status = fixResult.success ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
51
|
+
console.log(` ${status} ${finding.ruleId} ${finding.file}:${finding.line}`);
|
|
52
|
+
if (!fixResult.success && fixResult.error) {
|
|
53
|
+
console.log(` \x1b[33m${fixResult.error}\x1b[0m`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (validFixes.length === 0) {
|
|
57
|
+
console.log('\nNo fixes could be generated.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`\n${validFixes.length} fixes ready to apply.`);
|
|
61
|
+
// Dry run mode - just show diffs
|
|
62
|
+
if (options.dryRun) {
|
|
63
|
+
console.log('\n--dry-run: Showing diffs only:\n');
|
|
64
|
+
for (const fix of validFixes) {
|
|
65
|
+
console.log(printDiff(fix));
|
|
66
|
+
}
|
|
67
|
+
console.log('\nNo changes were made (dry-run mode).');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// If not --yes, prompt for confirmation
|
|
71
|
+
if (!options.yes) {
|
|
72
|
+
console.log('\nDiffs:');
|
|
73
|
+
for (const fix of validFixes) {
|
|
74
|
+
console.log(printDiff(fix));
|
|
75
|
+
}
|
|
76
|
+
const confirmed = await promptConfirm('\nApply these fixes?');
|
|
77
|
+
if (!confirmed) {
|
|
78
|
+
console.log('Aborted.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Group fixes by file
|
|
83
|
+
const byFile = new Map();
|
|
84
|
+
for (const fix of validFixes) {
|
|
85
|
+
const existing = byFile.get(fix.file) || [];
|
|
86
|
+
existing.push(fix);
|
|
87
|
+
byFile.set(fix.file, existing);
|
|
88
|
+
}
|
|
89
|
+
// Apply fixes
|
|
90
|
+
let successCount = 0;
|
|
91
|
+
let failCount = 0;
|
|
92
|
+
const modifiedFiles = [];
|
|
93
|
+
for (const [file, fileFixes] of byFile) {
|
|
94
|
+
const result = applyFixes(file, fileFixes);
|
|
95
|
+
if (result.applied) {
|
|
96
|
+
successCount += result.fixes.length;
|
|
97
|
+
modifiedFiles.push(file);
|
|
98
|
+
}
|
|
99
|
+
else if (result.error) {
|
|
100
|
+
console.error(`\x1b[31mError fixing ${file}: ${result.error}\x1b[0m`);
|
|
101
|
+
failCount += fileFixes.length;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
console.log(`\n\x1b[32m✓ ${successCount} fixes applied\x1b[0m`);
|
|
105
|
+
if (failCount > 0) {
|
|
106
|
+
console.log(`\x1b[31m✗ ${failCount} fixes failed\x1b[0m`);
|
|
107
|
+
}
|
|
108
|
+
// Git staging if requested
|
|
109
|
+
if (options.git && modifiedFiles.length > 0) {
|
|
110
|
+
try {
|
|
111
|
+
for (const file of modifiedFiles) {
|
|
112
|
+
execSync(`git add "${file}"`, { stdio: 'pipe' });
|
|
113
|
+
}
|
|
114
|
+
console.log('\nChanges staged for commit.');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('\x1b[33mWarning: Could not stage files with git.\x1b[0m');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Show verification
|
|
121
|
+
console.log('\nRe-scanning to verify fixes...');
|
|
122
|
+
const verifyResult = await scan(config);
|
|
123
|
+
const remainingFixable = getFixableFindings(verifyResult.findings);
|
|
124
|
+
if (remainingFixable.length === 0) {
|
|
125
|
+
console.log('\x1b[32m✓ All fixable issues have been resolved!\x1b[0m');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(`\x1b[33m${remainingFixable.length} fixable issues remain.\x1b[0m`);
|
|
129
|
+
}
|
|
130
|
+
// Exit code based on remaining issues
|
|
131
|
+
if (verifyResult.criticalCount > 0) {
|
|
132
|
+
process.exitCode = 2;
|
|
133
|
+
}
|
|
134
|
+
else if (verifyResult.warningCount > 0) {
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
process.exitCode = 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { installGitHubWorkflow } from '../../github/installer.js';
|
|
3
|
+
export async function githubCommand(action, targetPath = '.', options) {
|
|
4
|
+
const resolvedPath = resolve(targetPath);
|
|
5
|
+
if (action === 'install') {
|
|
6
|
+
try {
|
|
7
|
+
installGitHubWorkflow(resolvedPath, {
|
|
8
|
+
autoFix: options.autoFix ?? true,
|
|
9
|
+
severityThreshold: options.severity ?? 'warning',
|
|
10
|
+
aiVerify: options.aiVerify ?? false,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const err = error;
|
|
15
|
+
console.error(`Error: ${err.message}`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.error(`Unknown github action: ${action}`);
|
|
21
|
+
console.error('Usage: vibeguard github install [path]');
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
}
|
|
24
|
+
}
|