@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
|
@@ -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,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
|
+
}
|