@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.
Files changed (93) hide show
  1. package/README.md +162 -0
  2. package/bin/vibeguard.js +2 -0
  3. package/dist/ai/cache.d.ts +5 -0
  4. package/dist/ai/cache.js +20 -0
  5. package/dist/ai/index.d.ts +9 -0
  6. package/dist/ai/index.js +71 -0
  7. package/dist/ai/prompts.d.ts +7 -0
  8. package/dist/ai/prompts.js +65 -0
  9. package/dist/ai/provider.d.ts +12 -0
  10. package/dist/ai/provider.js +93 -0
  11. package/dist/ai/types.d.ts +21 -0
  12. package/dist/ai/types.js +1 -0
  13. package/dist/cli/commands/fix.d.ts +7 -0
  14. package/dist/cli/commands/fix.js +140 -0
  15. package/dist/cli/commands/github.d.ts +6 -0
  16. package/dist/cli/commands/github.js +24 -0
  17. package/dist/cli/commands/scan.d.ts +5 -0
  18. package/dist/cli/commands/scan.js +54 -0
  19. package/dist/cli/index.d.ts +1 -0
  20. package/dist/cli/index.js +49 -0
  21. package/dist/cli/options.d.ts +17 -0
  22. package/dist/cli/options.js +27 -0
  23. package/dist/config/defaults.d.ts +17 -0
  24. package/dist/config/defaults.js +21 -0
  25. package/dist/config/index.d.ts +17 -0
  26. package/dist/config/index.js +119 -0
  27. package/dist/config/schema.d.ts +20 -0
  28. package/dist/config/schema.js +39 -0
  29. package/dist/engine/file-walker.d.ts +12 -0
  30. package/dist/engine/file-walker.js +61 -0
  31. package/dist/engine/filter.d.ts +3 -0
  32. package/dist/engine/filter.js +50 -0
  33. package/dist/engine/index.d.ts +10 -0
  34. package/dist/engine/index.js +54 -0
  35. package/dist/engine/matcher.d.ts +10 -0
  36. package/dist/engine/matcher.js +47 -0
  37. package/dist/fix/engine.d.ts +37 -0
  38. package/dist/fix/engine.js +121 -0
  39. package/dist/fix/index.d.ts +2 -0
  40. package/dist/fix/index.js +2 -0
  41. package/dist/fix/patch.d.ts +23 -0
  42. package/dist/fix/patch.js +94 -0
  43. package/dist/fix/strategies.d.ts +21 -0
  44. package/dist/fix/strategies.js +213 -0
  45. package/dist/fix/types.d.ts +48 -0
  46. package/dist/fix/types.js +1 -0
  47. package/dist/github/client.d.ts +10 -0
  48. package/dist/github/client.js +43 -0
  49. package/dist/github/comment-formatter.d.ts +3 -0
  50. package/dist/github/comment-formatter.js +65 -0
  51. package/dist/github/index.d.ts +5 -0
  52. package/dist/github/index.js +5 -0
  53. package/dist/github/installer.d.ts +2 -0
  54. package/dist/github/installer.js +41 -0
  55. package/dist/github/types.d.ts +40 -0
  56. package/dist/github/types.js +1 -0
  57. package/dist/github/workflow-generator.d.ts +2 -0
  58. package/dist/github/workflow-generator.js +108 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.js +2 -0
  61. package/dist/reporters/console.d.ts +9 -0
  62. package/dist/reporters/console.js +76 -0
  63. package/dist/reporters/index.d.ts +6 -0
  64. package/dist/reporters/index.js +17 -0
  65. package/dist/reporters/json.d.ts +5 -0
  66. package/dist/reporters/json.js +32 -0
  67. package/dist/reporters/sarif.d.ts +9 -0
  68. package/dist/reporters/sarif.js +78 -0
  69. package/dist/reporters/types.d.ts +5 -0
  70. package/dist/reporters/types.js +1 -0
  71. package/dist/rules/config.d.ts +2 -0
  72. package/dist/rules/config.js +31 -0
  73. package/dist/rules/dependencies.d.ts +2 -0
  74. package/dist/rules/dependencies.js +32 -0
  75. package/dist/rules/docker.d.ts +2 -0
  76. package/dist/rules/docker.js +44 -0
  77. package/dist/rules/index.d.ts +5 -0
  78. package/dist/rules/index.js +25 -0
  79. package/dist/rules/kubernetes.d.ts +2 -0
  80. package/dist/rules/kubernetes.js +44 -0
  81. package/dist/rules/node.d.ts +2 -0
  82. package/dist/rules/node.js +72 -0
  83. package/dist/rules/python.d.ts +2 -0
  84. package/dist/rules/python.js +91 -0
  85. package/dist/rules/secrets.d.ts +2 -0
  86. package/dist/rules/secrets.js +82 -0
  87. package/dist/rules/types.d.ts +75 -0
  88. package/dist/rules/types.js +1 -0
  89. package/dist/utils/binary-check.d.ts +1 -0
  90. package/dist/utils/binary-check.js +10 -0
  91. package/dist/utils/line-mapper.d.ts +6 -0
  92. package/dist/utils/line-mapper.js +40 -0
  93. 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli/index.js';
@@ -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;
@@ -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';
@@ -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
+ }
@@ -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,6 @@
1
+ export interface GitHubCommandOptions {
2
+ autoFix?: boolean;
3
+ severity?: 'critical' | 'warning' | 'info';
4
+ aiVerify?: boolean;
5
+ }
6
+ export declare function githubCommand(action: string, targetPath: string | undefined, options: GitHubCommandOptions): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import { type CLIOptions } from '../../config/index.js';
2
+ export interface ScanCommandOptions extends CLIOptions {
3
+ color?: boolean;
4
+ }
5
+ export declare function scanCommand(targetPath: string | undefined, options: ScanCommandOptions): Promise<void>;