@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
@@ -0,0 +1,37 @@
1
+ import type { Finding, DetectionRule } from '../rules/types.js';
2
+ import type { Fix, FixResult, FixOptions, ApplyResult } from './types.js';
3
+ /**
4
+ * Check if a finding is fixable
5
+ */
6
+ export declare function isFixable(finding: Finding): boolean;
7
+ /**
8
+ * Get all fixable findings from a list
9
+ */
10
+ export declare function getFixableFindings(findings: Finding[]): Finding[];
11
+ /**
12
+ * Generate a fix for a single finding
13
+ */
14
+ export declare function generateFix(finding: Finding, basePath?: string): FixResult;
15
+ /**
16
+ * Generate fixes for multiple findings
17
+ */
18
+ export declare function generateFixes(findings: Finding[], basePath?: string): {
19
+ finding: Finding;
20
+ result: FixResult;
21
+ }[];
22
+ /**
23
+ * Apply fixes to files
24
+ * Groups fixes by file and applies them
25
+ */
26
+ export declare function applyAllFixes(fixes: Fix[], options?: FixOptions): ApplyResult[];
27
+ /**
28
+ * Print diff for a fix
29
+ */
30
+ export declare function printDiff(fix: Fix): string;
31
+ /**
32
+ * Get a summary of fixable rules
33
+ */
34
+ export declare function getFixableRules(): DetectionRule[];
35
+ export { generateUnifiedDiff, previewFix, applyFixes, dryRunFixes } from './patch.js';
36
+ export { getStrategy, strategies } from './strategies.js';
37
+ export type { Fix, FixResult, FixOptions, ApplyResult, FixStrategy } from './types.js';
@@ -0,0 +1,121 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, isAbsolute } from 'path';
3
+ import { getStrategy } from './strategies.js';
4
+ import { applyFixes, generateUnifiedDiff } from './patch.js';
5
+ import { allRules, ruleById } from '../rules/index.js';
6
+ /**
7
+ * Get the rule for a finding
8
+ */
9
+ function getRuleForFinding(finding) {
10
+ return ruleById.get(finding.ruleId);
11
+ }
12
+ /**
13
+ * Check if a finding is fixable
14
+ */
15
+ export function isFixable(finding) {
16
+ const rule = getRuleForFinding(finding);
17
+ return !!(rule?.fixable && rule?.fixStrategy);
18
+ }
19
+ /**
20
+ * Get all fixable findings from a list
21
+ */
22
+ export function getFixableFindings(findings) {
23
+ return findings.filter(isFixable);
24
+ }
25
+ /**
26
+ * Generate a fix for a single finding
27
+ */
28
+ export function generateFix(finding, basePath) {
29
+ const rule = getRuleForFinding(finding);
30
+ if (!rule?.fixable || !rule?.fixStrategy) {
31
+ return { success: false, fix: null, error: 'Rule is not fixable' };
32
+ }
33
+ const strategy = getStrategy(rule.fixStrategy);
34
+ if (!strategy) {
35
+ return { success: false, fix: null, error: `Unknown fix strategy: ${rule.fixStrategy}` };
36
+ }
37
+ // Resolve file path
38
+ const filePath = resolveFilePath(finding.file, basePath);
39
+ // Read file content
40
+ let content;
41
+ try {
42
+ content = readFileSync(filePath, 'utf-8');
43
+ }
44
+ catch (error) {
45
+ return {
46
+ success: false,
47
+ fix: null,
48
+ error: `Could not read file: ${filePath}`,
49
+ };
50
+ }
51
+ // Check if strategy can fix this finding
52
+ if (!strategy.canFix(finding, content)) {
53
+ return { success: false, fix: null, error: 'Strategy cannot fix this specific case' };
54
+ }
55
+ // Generate the fix
56
+ const result = strategy.generateFix(finding, content);
57
+ // Update file path in fix to be absolute
58
+ if (result.success && result.fix) {
59
+ result.fix.file = filePath;
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Generate fixes for multiple findings
65
+ */
66
+ export function generateFixes(findings, basePath) {
67
+ const results = [];
68
+ for (const finding of findings) {
69
+ const result = generateFix(finding, basePath);
70
+ results.push({ finding, result });
71
+ }
72
+ return results;
73
+ }
74
+ /**
75
+ * Apply fixes to files
76
+ * Groups fixes by file and applies them
77
+ */
78
+ export function applyAllFixes(fixes, options = {}) {
79
+ const results = [];
80
+ // Group fixes by file
81
+ const byFile = new Map();
82
+ for (const fix of fixes) {
83
+ const existing = byFile.get(fix.file) || [];
84
+ existing.push(fix);
85
+ byFile.set(fix.file, existing);
86
+ }
87
+ // Apply fixes to each file
88
+ for (const [file, fileFixes] of byFile) {
89
+ if (options.dryRun) {
90
+ // Just report what would be done
91
+ results.push({ file, fixes: fileFixes, applied: false });
92
+ }
93
+ else {
94
+ const result = applyFixes(file, fileFixes);
95
+ results.push(result);
96
+ }
97
+ }
98
+ return results;
99
+ }
100
+ /**
101
+ * Print diff for a fix
102
+ */
103
+ export function printDiff(fix) {
104
+ return generateUnifiedDiff(fix);
105
+ }
106
+ /**
107
+ * Get a summary of fixable rules
108
+ */
109
+ export function getFixableRules() {
110
+ return allRules.filter(r => r.fixable && r.fixStrategy);
111
+ }
112
+ // Helper function
113
+ function resolveFilePath(file, basePath) {
114
+ if (isAbsolute(file)) {
115
+ return file;
116
+ }
117
+ return basePath ? join(basePath, file) : file;
118
+ }
119
+ // Re-exports
120
+ export { generateUnifiedDiff, previewFix, applyFixes, dryRunFixes } from './patch.js';
121
+ export { getStrategy, strategies } from './strategies.js';
@@ -0,0 +1,2 @@
1
+ export { isFixable, getFixableFindings, generateFix, generateFixes, applyAllFixes, printDiff, getFixableRules, generateUnifiedDiff, previewFix, applyFixes, dryRunFixes, getStrategy, strategies, } from './engine.js';
2
+ export type { Fix, FixResult, FixOptions, ApplyResult, FixStrategy, } from './types.js';
@@ -0,0 +1,2 @@
1
+ // Fix Engine - Auto-fix security issues
2
+ export { isFixable, getFixableFindings, generateFix, generateFixes, applyAllFixes, printDiff, getFixableRules, generateUnifiedDiff, previewFix, applyFixes, dryRunFixes, getStrategy, strategies, } from './engine.js';
@@ -0,0 +1,23 @@
1
+ import type { Fix, ApplyResult } from './types.js';
2
+ /**
3
+ * Generate a unified diff string for a fix
4
+ */
5
+ export declare function generateUnifiedDiff(fix: Fix, content?: string): string;
6
+ /**
7
+ * Preview a fix showing before/after
8
+ */
9
+ export declare function previewFix(fix: Fix): string;
10
+ /**
11
+ * Apply a single fix to file content
12
+ * Returns the modified content
13
+ */
14
+ export declare function applyFixToContent(content: string, fix: Fix): string;
15
+ /**
16
+ * Apply multiple fixes to a file
17
+ * Fixes must be sorted by start offset (descending) to avoid offset corruption
18
+ */
19
+ export declare function applyFixes(filePath: string, fixes: Fix[]): ApplyResult;
20
+ /**
21
+ * Apply fixes in dry-run mode (returns what would be changed)
22
+ */
23
+ export declare function dryRunFixes(filePath: string, fixes: Fix[], content: string): string;
@@ -0,0 +1,94 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ /**
3
+ * Generate a unified diff string for a fix
4
+ */
5
+ export function generateUnifiedDiff(fix, content) {
6
+ const original = fix.original;
7
+ const replacement = fix.replacement;
8
+ // Simple unified diff format
9
+ const lines = [];
10
+ lines.push(`--- a/${fix.file}`);
11
+ lines.push(`+++ b/${fix.file}`);
12
+ lines.push(`@@ -1,1 +1,1 @@`);
13
+ // Show original lines with -
14
+ for (const line of original.split('\n')) {
15
+ lines.push(`-${line}`);
16
+ }
17
+ // Show replacement lines with +
18
+ for (const line of replacement.split('\n')) {
19
+ lines.push(`+${line}`);
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+ /**
24
+ * Preview a fix showing before/after
25
+ */
26
+ export function previewFix(fix) {
27
+ const lines = [];
28
+ lines.push(`File: ${fix.file}`);
29
+ lines.push(`Rule: ${fix.ruleId}`);
30
+ lines.push(`Description: ${fix.description}`);
31
+ lines.push('');
32
+ lines.push('Before:');
33
+ lines.push(` ${fix.original}`);
34
+ lines.push('');
35
+ lines.push('After:');
36
+ lines.push(` ${fix.replacement}`);
37
+ return lines.join('\n');
38
+ }
39
+ /**
40
+ * Apply a single fix to file content
41
+ * Returns the modified content
42
+ */
43
+ export function applyFixToContent(content, fix) {
44
+ // Verify the original text is at the expected position
45
+ const actualText = content.slice(fix.start, fix.end);
46
+ if (actualText !== fix.original) {
47
+ throw new Error(`Content mismatch at offset ${fix.start}. ` +
48
+ `Expected "${fix.original}" but found "${actualText}"`);
49
+ }
50
+ // Apply the fix
51
+ return content.slice(0, fix.start) + fix.replacement + content.slice(fix.end);
52
+ }
53
+ /**
54
+ * Apply multiple fixes to a file
55
+ * Fixes must be sorted by start offset (descending) to avoid offset corruption
56
+ */
57
+ export function applyFixes(filePath, fixes) {
58
+ if (fixes.length === 0) {
59
+ return { file: filePath, fixes: [], applied: false };
60
+ }
61
+ try {
62
+ let content = readFileSync(filePath, 'utf-8');
63
+ // Sort fixes by start offset descending (apply from end to beginning)
64
+ const sortedFixes = [...fixes].sort((a, b) => b.start - a.start);
65
+ // Apply each fix
66
+ for (const fix of sortedFixes) {
67
+ content = applyFixToContent(content, fix);
68
+ }
69
+ // Write the modified content
70
+ writeFileSync(filePath, content);
71
+ return { file: filePath, fixes, applied: true };
72
+ }
73
+ catch (error) {
74
+ return {
75
+ file: filePath,
76
+ fixes,
77
+ applied: false,
78
+ error: error instanceof Error ? error.message : 'Unknown error',
79
+ };
80
+ }
81
+ }
82
+ /**
83
+ * Apply fixes in dry-run mode (returns what would be changed)
84
+ */
85
+ export function dryRunFixes(filePath, fixes, content) {
86
+ // Sort fixes by start offset descending
87
+ const sortedFixes = [...fixes].sort((a, b) => b.start - a.start);
88
+ // Apply each fix to get the final content
89
+ let modifiedContent = content;
90
+ for (const fix of sortedFixes) {
91
+ modifiedContent = applyFixToContent(modifiedContent, fix);
92
+ }
93
+ return modifiedContent;
94
+ }
@@ -0,0 +1,21 @@
1
+ import type { FixStrategy } from './types.js';
2
+ /**
3
+ * Strategy: eval(x) -> JSON.parse(x)
4
+ * Works for both JS and Python
5
+ */
6
+ export declare const evalToJsonParse: FixStrategy;
7
+ /**
8
+ * Strategy: hardcoded secret -> environment variable
9
+ * Handles both JS (process.env) and Python (os.environ.get)
10
+ */
11
+ export declare const hardcodedToEnv: FixStrategy;
12
+ /**
13
+ * Strategy: shell=True -> shell=False
14
+ */
15
+ export declare const shellTrueToFalse: FixStrategy;
16
+ /**
17
+ * Strategy: Remove verify=False from requests calls
18
+ */
19
+ export declare const removeVerifyFalse: FixStrategy;
20
+ export declare const strategies: Map<string, FixStrategy>;
21
+ export declare function getStrategy(name: string): FixStrategy | undefined;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Strategy: eval(x) -> JSON.parse(x)
3
+ * Works for both JS and Python
4
+ */
5
+ export const evalToJsonParse = {
6
+ name: 'eval-to-json-parse',
7
+ canFix(finding, content) {
8
+ // Must have a valid match offset
9
+ if (finding.matchOffset === undefined)
10
+ return false;
11
+ // Must contain eval(
12
+ return /\beval\s*\(/.test(finding.match);
13
+ },
14
+ generateFix(finding, content) {
15
+ if (finding.matchOffset === undefined) {
16
+ return { success: false, fix: null, error: 'No match offset available' };
17
+ }
18
+ const start = finding.matchOffset;
19
+ const matchLen = finding.match.length;
20
+ // Find the full eval(...) call including the closing paren
21
+ // We need to find the matching closing parenthesis
22
+ let depth = 0;
23
+ let endPos = start;
24
+ let foundOpen = false;
25
+ for (let i = start; i < content.length; i++) {
26
+ const char = content[i];
27
+ if (char === '(') {
28
+ depth++;
29
+ foundOpen = true;
30
+ }
31
+ else if (char === ')') {
32
+ depth--;
33
+ if (foundOpen && depth === 0) {
34
+ endPos = i + 1;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ if (endPos <= start) {
40
+ return { success: false, fix: null, error: 'Could not find closing parenthesis' };
41
+ }
42
+ const original = content.slice(start, endPos);
43
+ // Extract the argument: eval(arg) -> arg
44
+ const argMatch = original.match(/\beval\s*\(([\s\S]*)\)$/);
45
+ if (!argMatch) {
46
+ return { success: false, fix: null, error: 'Could not parse eval argument' };
47
+ }
48
+ const arg = argMatch[1];
49
+ const replacement = `JSON.parse(${arg})`;
50
+ return {
51
+ success: true,
52
+ fix: {
53
+ file: finding.file,
54
+ start,
55
+ end: endPos,
56
+ original,
57
+ replacement,
58
+ ruleId: finding.ruleId,
59
+ description: 'Replace eval() with JSON.parse()',
60
+ },
61
+ };
62
+ },
63
+ };
64
+ /**
65
+ * Strategy: hardcoded secret -> environment variable
66
+ * Handles both JS (process.env) and Python (os.environ.get)
67
+ */
68
+ export const hardcodedToEnv = {
69
+ name: 'hardcoded-to-env',
70
+ canFix(finding, content) {
71
+ if (finding.matchOffset === undefined)
72
+ return false;
73
+ // Must be an assignment with a string value
74
+ return /(?:api[_-]?key|secret|password|token|auth[_-]?token|private[_-]?key)\s*[:=]\s*['"][^'"]+['"]/i.test(finding.match);
75
+ },
76
+ generateFix(finding, content) {
77
+ if (finding.matchOffset === undefined) {
78
+ return { success: false, fix: null, error: 'No match offset available' };
79
+ }
80
+ const start = finding.matchOffset;
81
+ const original = finding.match;
82
+ const end = start + original.length;
83
+ // Extract the variable name
84
+ const varMatch = original.match(/^(\w+)\s*[:=]/);
85
+ if (!varMatch) {
86
+ return { success: false, fix: null, error: 'Could not extract variable name' };
87
+ }
88
+ const varName = varMatch[1];
89
+ const envVarName = toScreamingSnakeCase(varName);
90
+ // Detect language from file extension
91
+ const isPython = finding.file.endsWith('.py');
92
+ const isJS = /\.(js|ts|jsx|tsx|mjs|cjs)$/.test(finding.file);
93
+ let replacement;
94
+ if (isPython) {
95
+ // Python: api_key = os.environ.get("API_KEY")
96
+ replacement = `${varName} = os.environ.get("${envVarName}")`;
97
+ }
98
+ else if (isJS) {
99
+ // JavaScript: const token = process.env.TOKEN
100
+ // Preserve the declaration keyword if present
101
+ const declMatch = original.match(/^(const|let|var)\s+/);
102
+ const prefix = declMatch ? declMatch[1] + ' ' : '';
103
+ replacement = `${prefix}${varName} = process.env.${envVarName}`;
104
+ }
105
+ else {
106
+ return { success: false, fix: null, error: 'Unsupported file type' };
107
+ }
108
+ return {
109
+ success: true,
110
+ fix: {
111
+ file: finding.file,
112
+ start,
113
+ end,
114
+ original,
115
+ replacement,
116
+ ruleId: finding.ruleId,
117
+ description: `Replace hardcoded value with ${isPython ? 'os.environ.get' : 'process.env'}.${envVarName}`,
118
+ },
119
+ };
120
+ },
121
+ };
122
+ /**
123
+ * Strategy: shell=True -> shell=False
124
+ */
125
+ export const shellTrueToFalse = {
126
+ name: 'shell-true-to-false',
127
+ canFix(finding, content) {
128
+ if (finding.matchOffset === undefined)
129
+ return false;
130
+ return /shell\s*=\s*True/i.test(finding.match);
131
+ },
132
+ generateFix(finding, content) {
133
+ if (finding.matchOffset === undefined) {
134
+ return { success: false, fix: null, error: 'No match offset available' };
135
+ }
136
+ const start = finding.matchOffset;
137
+ const original = finding.match;
138
+ const end = start + original.length;
139
+ // Replace shell=True with shell=False
140
+ const replacement = original.replace(/shell\s*=\s*True/i, 'shell=False');
141
+ if (replacement === original) {
142
+ return { success: false, fix: null, error: 'Could not find shell=True to replace' };
143
+ }
144
+ return {
145
+ success: true,
146
+ fix: {
147
+ file: finding.file,
148
+ start,
149
+ end,
150
+ original,
151
+ replacement,
152
+ ruleId: finding.ruleId,
153
+ description: 'Set shell=False (command may need to be split into list)',
154
+ },
155
+ };
156
+ },
157
+ };
158
+ /**
159
+ * Strategy: Remove verify=False from requests calls
160
+ */
161
+ export const removeVerifyFalse = {
162
+ name: 'remove-verify-false',
163
+ canFix(finding, content) {
164
+ if (finding.matchOffset === undefined)
165
+ return false;
166
+ return /verify\s*=\s*False/i.test(finding.match);
167
+ },
168
+ generateFix(finding, content) {
169
+ if (finding.matchOffset === undefined) {
170
+ return { success: false, fix: null, error: 'No match offset available' };
171
+ }
172
+ const start = finding.matchOffset;
173
+ const original = finding.match;
174
+ const end = start + original.length;
175
+ // Remove verify=False (and any preceding comma+space or following comma+space)
176
+ let replacement = original.replace(/,\s*verify\s*=\s*False/i, '');
177
+ if (replacement === original) {
178
+ replacement = original.replace(/verify\s*=\s*False\s*,?\s*/i, '');
179
+ }
180
+ if (replacement === original) {
181
+ return { success: false, fix: null, error: 'Could not remove verify=False' };
182
+ }
183
+ return {
184
+ success: true,
185
+ fix: {
186
+ file: finding.file,
187
+ start,
188
+ end,
189
+ original,
190
+ replacement,
191
+ ruleId: finding.ruleId,
192
+ description: 'Remove verify=False to enable SSL verification',
193
+ },
194
+ };
195
+ },
196
+ };
197
+ // Helper function
198
+ function toScreamingSnakeCase(str) {
199
+ return str
200
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
201
+ .replace(/[-\s]/g, '_')
202
+ .toUpperCase();
203
+ }
204
+ // Strategy registry
205
+ export const strategies = new Map([
206
+ ['eval-to-json-parse', evalToJsonParse],
207
+ ['hardcoded-to-env', hardcodedToEnv],
208
+ ['shell-true-to-false', shellTrueToFalse],
209
+ ['remove-verify-false', removeVerifyFalse],
210
+ ]);
211
+ export function getStrategy(name) {
212
+ return strategies.get(name);
213
+ }
@@ -0,0 +1,48 @@
1
+ import type { Finding } from '../rules/types.js';
2
+ /**
3
+ * Represents a single fix to apply to a file
4
+ */
5
+ export interface Fix {
6
+ file: string;
7
+ start: number;
8
+ end: number;
9
+ original: string;
10
+ replacement: string;
11
+ ruleId: string;
12
+ description: string;
13
+ }
14
+ /**
15
+ * Result of attempting to generate a fix
16
+ */
17
+ export interface FixResult {
18
+ success: boolean;
19
+ fix: Fix | null;
20
+ error?: string;
21
+ }
22
+ /**
23
+ * A fix strategy transforms a finding into a fix
24
+ */
25
+ export interface FixStrategy {
26
+ name: string;
27
+ canFix(finding: Finding, content: string): boolean;
28
+ generateFix(finding: Finding, content: string): FixResult;
29
+ }
30
+ /**
31
+ * Options for the fix command
32
+ */
33
+ export interface FixOptions {
34
+ dryRun?: boolean;
35
+ yes?: boolean;
36
+ git?: boolean;
37
+ basePath?: string;
38
+ verbose?: boolean;
39
+ }
40
+ /**
41
+ * Result of applying fixes to a file
42
+ */
43
+ export interface ApplyResult {
44
+ file: string;
45
+ fixes: Fix[];
46
+ applied: boolean;
47
+ error?: string;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { GitHubConfig, PR, PRFile, ReviewInput } from './types.js';
2
+ export declare class GitHubClient {
3
+ private config;
4
+ constructor(config: GitHubConfig);
5
+ private fetch;
6
+ getPullRequest(prNumber: number): Promise<PR>;
7
+ getPullRequestFiles(prNumber: number): Promise<PRFile[]>;
8
+ postComment(issueNumber: number, body: string): Promise<void>;
9
+ createReview(prNumber: number, review: ReviewInput): Promise<void>;
10
+ }
@@ -0,0 +1,43 @@
1
+ export class GitHubClient {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ async fetch(endpoint, options = {}) {
7
+ const [owner, repo] = this.config.repo.split('/');
8
+ const url = `https://api.github.com/repos/${owner}/${repo}${endpoint}`;
9
+ const response = await fetch(url, {
10
+ ...options,
11
+ headers: {
12
+ 'Authorization': `Bearer ${this.config.token}`,
13
+ 'Accept': 'application/vnd.github+json',
14
+ 'X-GitHub-Api-Version': '2022-11-28',
15
+ 'Content-Type': 'application/json',
16
+ ...options.headers,
17
+ },
18
+ });
19
+ if (!response.ok) {
20
+ const error = await response.text();
21
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText} - ${error}`);
22
+ }
23
+ return response.json();
24
+ }
25
+ async getPullRequest(prNumber) {
26
+ return this.fetch(`/pulls/${prNumber}`);
27
+ }
28
+ async getPullRequestFiles(prNumber) {
29
+ return this.fetch(`/pulls/${prNumber}/files`);
30
+ }
31
+ async postComment(issueNumber, body) {
32
+ await this.fetch(`/issues/${issueNumber}/comments`, {
33
+ method: 'POST',
34
+ body: JSON.stringify({ body }),
35
+ });
36
+ }
37
+ async createReview(prNumber, review) {
38
+ await this.fetch(`/pulls/${prNumber}/reviews`, {
39
+ method: 'POST',
40
+ body: JSON.stringify(review),
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScanResult, Finding } from '../rules/types.js';
2
+ export declare function formatPRComment(result: ScanResult): string;
3
+ export declare function formatInlineComment(finding: Finding): string;