@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,54 @@
1
+ import { resolve } from 'node:path';
2
+ import { scan } from '../../engine/index.js';
3
+ import { resolveConfig } from '../../config/index.js';
4
+ import { createReporter } from '../../reporters/index.js';
5
+ import { verifyFindings, detectProvider } from '../../ai/index.js';
6
+ import { ruleById } from '../../rules/index.js';
7
+ export async function scanCommand(targetPath = '.', options) {
8
+ const resolvedPath = resolve(targetPath);
9
+ // Handle --no-color (commander sets color: false)
10
+ if (options.color === false) {
11
+ options.noColor = true;
12
+ }
13
+ const config = resolveConfig(resolvedPath, options);
14
+ if (config.verbose) {
15
+ console.error(`VibeGuard scanning: ${config.targetPath}`);
16
+ console.error(`Ignore patterns: ${config.ignorePatterns.length}`);
17
+ console.error(`Max file size: ${config.maxFileSize}`);
18
+ console.error(`Severities: ${config.includeSeverities.join(', ')}`);
19
+ console.error(`Format: ${config.format}`);
20
+ console.error(`AI verification: ${config.aiVerify ? 'enabled' : 'disabled'}`);
21
+ }
22
+ let result = await scan(config);
23
+ // AI verification if enabled and API key available
24
+ if (config.aiVerify && config.aiApiKey) {
25
+ if (config.verbose) {
26
+ console.error(`Running AI verification with ${config.aiProvider || 'auto-detected'} provider...`);
27
+ }
28
+ const provider = config.aiProvider || detectProvider(config.aiApiKey);
29
+ const verifiedFindings = await verifyFindings(result.findings, ruleById, {
30
+ provider,
31
+ apiKey: config.aiApiKey,
32
+ }, config.verbose);
33
+ result = {
34
+ ...result,
35
+ findings: verifiedFindings,
36
+ };
37
+ }
38
+ else if (config.aiVerify && !config.aiApiKey) {
39
+ console.error('Warning: --ai flag set but no API key found. Set VIBEGUARD_AI_KEY or use --ai-key.');
40
+ }
41
+ const reporter = createReporter(config.format, config.noColor);
42
+ const output = reporter.report(result);
43
+ console.log(output);
44
+ // Exit codes: 0 = clean/info only, 1 = warnings, 2 = critical
45
+ if (result.criticalCount > 0) {
46
+ process.exitCode = 2;
47
+ }
48
+ else if (result.warningCount > 0) {
49
+ process.exitCode = 1;
50
+ }
51
+ else {
52
+ process.exitCode = 0;
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { Command } from 'commander';
2
+ import { VERSION, NAME } from '../index.js';
3
+ import { scanCommand } from './commands/scan.js';
4
+ import { fixCommand } from './commands/fix.js';
5
+ import { githubCommand } from './commands/github.js';
6
+ import { severityOption, formatOption, jsonOption, sarifOption, ignoreOption, maxFileSizeOption, noColorOption, verboseOption, configOption, aiOption, aiKeyOption, aiProviderOption, dryRunOption, yesOption, gitOption, } from './options.js';
7
+ const program = new Command();
8
+ program
9
+ .name(NAME)
10
+ .version(VERSION)
11
+ .description('Regex-first security scanner for AI-generated code');
12
+ program
13
+ .command('scan', { isDefault: true })
14
+ .description('Scan a directory for security issues')
15
+ .argument('[path]', 'directory to scan', '.')
16
+ .addOption(severityOption)
17
+ .addOption(formatOption)
18
+ .addOption(jsonOption)
19
+ .addOption(sarifOption)
20
+ .addOption(ignoreOption)
21
+ .addOption(maxFileSizeOption)
22
+ .addOption(noColorOption)
23
+ .addOption(verboseOption)
24
+ .addOption(configOption)
25
+ .addOption(aiOption)
26
+ .addOption(aiKeyOption)
27
+ .addOption(aiProviderOption)
28
+ .action(scanCommand);
29
+ program
30
+ .command('fix')
31
+ .description('Auto-fix security issues in code')
32
+ .argument('[path]', 'directory to fix', '.')
33
+ .addOption(severityOption)
34
+ .addOption(dryRunOption)
35
+ .addOption(yesOption)
36
+ .addOption(gitOption)
37
+ .addOption(verboseOption)
38
+ .addOption(configOption)
39
+ .action(fixCommand);
40
+ program
41
+ .command('github')
42
+ .description('GitHub integration commands')
43
+ .argument('<action>', 'install')
44
+ .argument('[path]', 'project directory', '.')
45
+ .option('--no-auto-fix', 'disable automatic fixes on approval')
46
+ .option('--severity <level>', 'minimum severity to report', 'warning')
47
+ .option('--ai-verify', 'enable AI verification')
48
+ .action(githubCommand);
49
+ program.parse();
@@ -0,0 +1,17 @@
1
+ import { Option } from 'commander';
2
+ export declare const severityOption: Option;
3
+ export declare const formatOption: Option;
4
+ export declare const jsonOption: Option;
5
+ export declare const sarifOption: Option;
6
+ export declare const ignoreOption: Option;
7
+ export declare const maxFileSizeOption: Option;
8
+ export declare const noColorOption: Option;
9
+ export declare const verboseOption: Option;
10
+ export declare const aiOption: Option;
11
+ export declare const noAiOption: Option;
12
+ export declare const aiKeyOption: Option;
13
+ export declare const aiProviderOption: Option;
14
+ export declare const configOption: Option;
15
+ export declare const dryRunOption: Option;
16
+ export declare const yesOption: Option;
17
+ export declare const gitOption: Option;
@@ -0,0 +1,27 @@
1
+ import { Option } from 'commander';
2
+ import { validateSeverity, validateFormat, validateMaxFileSize } from '../config/schema.js';
3
+ export const severityOption = new Option('-s, --severity <level>', 'minimum severity level to report')
4
+ .choices(['critical', 'warning', 'info'])
5
+ .default('warning')
6
+ .argParser(validateSeverity);
7
+ export const formatOption = new Option('-f, --format <type>', 'output format')
8
+ .choices(['console', 'json', 'sarif'])
9
+ .default('console')
10
+ .argParser(validateFormat);
11
+ export const jsonOption = new Option('--json', 'shorthand for --format json');
12
+ export const sarifOption = new Option('--sarif', 'shorthand for --format sarif');
13
+ export const ignoreOption = new Option('-i, --ignore <pattern>', 'additional ignore patterns (can be repeated)');
14
+ export const maxFileSizeOption = new Option('--max-file-size <bytes>', 'skip files larger than this size')
15
+ .default('1048576')
16
+ .argParser(validateMaxFileSize);
17
+ export const noColorOption = new Option('--no-color', 'disable colored output');
18
+ export const verboseOption = new Option('-v, --verbose', 'show debug information');
19
+ export const aiOption = new Option('--ai', 'enable AI verification for flagged rules');
20
+ export const noAiOption = new Option('--no-ai', 'disable AI verification');
21
+ export const aiKeyOption = new Option('--ai-key <key>', 'API key for AI provider');
22
+ export const aiProviderOption = new Option('--ai-provider <name>', 'AI provider: openai, anthropic, groq').choices(['openai', 'anthropic', 'groq']);
23
+ export const configOption = new Option('-c, --config <path>', 'path to config file');
24
+ // Fix command options
25
+ export const dryRunOption = new Option('--dry-run', 'show diffs without applying changes');
26
+ export const yesOption = new Option('-y, --yes', 'apply fixes without prompting for confirmation');
27
+ export const gitOption = new Option('--git', 'stage fixed files with git add');
@@ -0,0 +1,17 @@
1
+ import type { RuleSeverity } from '../rules/types.js';
2
+ import type { ReporterType } from '../reporters/types.js';
3
+ export interface VibeGuardConfig {
4
+ targetPath: string;
5
+ ignorePatterns: string[];
6
+ maxFileSize: number;
7
+ includeSeverities: RuleSeverity[];
8
+ ruleIds?: string[];
9
+ format: ReporterType;
10
+ noColor: boolean;
11
+ verbose: boolean;
12
+ aiVerify: boolean;
13
+ aiProvider?: string;
14
+ aiApiKey?: string;
15
+ }
16
+ export declare const DEFAULT_CONFIG: VibeGuardConfig;
17
+ export declare const DEFAULT_IGNORE_PATTERNS: string[];
@@ -0,0 +1,21 @@
1
+ export const DEFAULT_CONFIG = {
2
+ targetPath: '.',
3
+ ignorePatterns: [],
4
+ maxFileSize: 1024 * 1024, // 1MB
5
+ includeSeverities: ['critical', 'warning'],
6
+ format: 'console',
7
+ noColor: false,
8
+ verbose: false,
9
+ aiVerify: false,
10
+ };
11
+ export const DEFAULT_IGNORE_PATTERNS = [
12
+ '**/node_modules/**',
13
+ '**/.git/**',
14
+ '**/dist/**',
15
+ '**/.next/**',
16
+ '**/build/**',
17
+ '**/coverage/**',
18
+ '**/.venv/**',
19
+ '**/__pycache__/**',
20
+ '**/vendor/**',
21
+ ];
@@ -0,0 +1,17 @@
1
+ import type { VibeGuardConfig } from './defaults.js';
2
+ export interface CLIOptions {
3
+ severity?: string;
4
+ format?: string;
5
+ json?: boolean;
6
+ sarif?: boolean;
7
+ ignore?: string[];
8
+ maxFileSize?: string;
9
+ noColor?: boolean;
10
+ verbose?: boolean;
11
+ ai?: boolean;
12
+ aiKey?: string;
13
+ aiProvider?: string;
14
+ config?: string;
15
+ }
16
+ export declare function resolveConfig(targetPath: string, cliOptions?: CLIOptions): VibeGuardConfig;
17
+ export { type VibeGuardConfig, DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } from './defaults.js';
@@ -0,0 +1,119 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } from './defaults.js';
4
+ import { parseSeverities, validateFormat } from './schema.js';
5
+ const CONFIG_FILENAMES = [
6
+ 'vibeguard.config.js',
7
+ 'vibeguard.config.mjs',
8
+ 'vibeguard.config.json',
9
+ '.vibeguardrc',
10
+ '.vibeguardrc.json',
11
+ ];
12
+ function findConfigFile(startDir) {
13
+ let currentDir = resolve(startDir);
14
+ const root = dirname(currentDir);
15
+ while (currentDir !== root) {
16
+ for (const filename of CONFIG_FILENAMES) {
17
+ const configPath = resolve(currentDir, filename);
18
+ if (existsSync(configPath)) {
19
+ return configPath;
20
+ }
21
+ }
22
+ const parent = dirname(currentDir);
23
+ if (parent === currentDir)
24
+ break;
25
+ currentDir = parent;
26
+ }
27
+ return null;
28
+ }
29
+ function loadConfigFile(configPath) {
30
+ const content = readFileSync(configPath, 'utf-8');
31
+ if (configPath.endsWith('.json') || configPath.endsWith('.vibeguardrc')) {
32
+ return JSON.parse(content);
33
+ }
34
+ // For JS/MJS files, we'd need dynamic import - skip for MVP
35
+ return {};
36
+ }
37
+ export function resolveConfig(targetPath, cliOptions = {}) {
38
+ // Start with defaults
39
+ const config = { ...DEFAULT_CONFIG };
40
+ config.targetPath = resolve(targetPath);
41
+ config.ignorePatterns = [...DEFAULT_IGNORE_PATTERNS];
42
+ // Load config file if exists
43
+ const configPath = cliOptions.config || findConfigFile(targetPath);
44
+ if (configPath && existsSync(configPath)) {
45
+ const fileConfig = loadConfigFile(configPath);
46
+ if (fileConfig.ignore) {
47
+ config.ignorePatterns.push(...fileConfig.ignore);
48
+ }
49
+ if (fileConfig.maxFileSize !== undefined) {
50
+ config.maxFileSize = fileConfig.maxFileSize;
51
+ }
52
+ if (fileConfig.severity) {
53
+ config.includeSeverities = parseSeverities(fileConfig.severity);
54
+ }
55
+ if (fileConfig.rules) {
56
+ config.ruleIds = fileConfig.rules;
57
+ }
58
+ if (fileConfig.format) {
59
+ config.format = validateFormat(fileConfig.format);
60
+ }
61
+ if (fileConfig.noColor !== undefined) {
62
+ config.noColor = fileConfig.noColor;
63
+ }
64
+ if (fileConfig.verbose !== undefined) {
65
+ config.verbose = fileConfig.verbose;
66
+ }
67
+ if (fileConfig.ai !== undefined) {
68
+ config.aiVerify = fileConfig.ai;
69
+ }
70
+ if (fileConfig.aiProvider) {
71
+ config.aiProvider = fileConfig.aiProvider;
72
+ }
73
+ if (fileConfig.aiApiKey) {
74
+ config.aiApiKey = fileConfig.aiApiKey;
75
+ }
76
+ }
77
+ // CLI options override everything
78
+ if (cliOptions.ignore) {
79
+ config.ignorePatterns.push(...cliOptions.ignore);
80
+ }
81
+ if (cliOptions.maxFileSize) {
82
+ config.maxFileSize = parseInt(cliOptions.maxFileSize, 10);
83
+ }
84
+ if (cliOptions.severity) {
85
+ config.includeSeverities = parseSeverities(cliOptions.severity);
86
+ }
87
+ if (cliOptions.json) {
88
+ config.format = 'json';
89
+ }
90
+ else if (cliOptions.sarif) {
91
+ config.format = 'sarif';
92
+ }
93
+ else if (cliOptions.format) {
94
+ config.format = validateFormat(cliOptions.format);
95
+ }
96
+ if (cliOptions.noColor) {
97
+ config.noColor = true;
98
+ }
99
+ if (cliOptions.verbose) {
100
+ config.verbose = true;
101
+ }
102
+ if (cliOptions.ai !== undefined) {
103
+ config.aiVerify = cliOptions.ai;
104
+ }
105
+ if (cliOptions.aiKey) {
106
+ config.aiApiKey = cliOptions.aiKey;
107
+ }
108
+ if (cliOptions.aiProvider) {
109
+ config.aiProvider = cliOptions.aiProvider;
110
+ }
111
+ // Env vars for AI key
112
+ if (!config.aiApiKey) {
113
+ config.aiApiKey = process.env.VIBEGUARD_AI_KEY ||
114
+ process.env.OPENAI_API_KEY ||
115
+ process.env.ANTHROPIC_API_KEY;
116
+ }
117
+ return config;
118
+ }
119
+ export { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } from './defaults.js';
@@ -0,0 +1,20 @@
1
+ import type { RuleSeverity } from '../rules/types.js';
2
+ import type { ReporterType } from '../reporters/types.js';
3
+ export interface ConfigFileSchema {
4
+ targetPath?: string;
5
+ ignore?: string[];
6
+ maxFileSize?: number;
7
+ severity?: RuleSeverity | RuleSeverity[];
8
+ rules?: string[];
9
+ format?: ReporterType;
10
+ noColor?: boolean;
11
+ verbose?: boolean;
12
+ ai?: boolean;
13
+ aiProvider?: string;
14
+ aiApiKey?: string;
15
+ }
16
+ export declare function validateSeverity(value: string): RuleSeverity;
17
+ export declare function validateFormat(value: string): ReporterType;
18
+ export declare function validateMaxFileSize(value: string): number;
19
+ export declare function severitiesAtOrAbove(minSeverity: RuleSeverity): RuleSeverity[];
20
+ export declare function parseSeverities(input: string | string[]): RuleSeverity[];
@@ -0,0 +1,39 @@
1
+ export function validateSeverity(value) {
2
+ const valid = ['critical', 'warning', 'info'];
3
+ if (!valid.includes(value)) {
4
+ throw new Error(`Invalid severity: ${value}. Must be one of: ${valid.join(', ')}`);
5
+ }
6
+ return value;
7
+ }
8
+ export function validateFormat(value) {
9
+ const valid = ['console', 'json', 'sarif'];
10
+ if (!valid.includes(value)) {
11
+ throw new Error(`Invalid format: ${value}. Must be one of: ${valid.join(', ')}`);
12
+ }
13
+ return value;
14
+ }
15
+ export function validateMaxFileSize(value) {
16
+ const num = parseInt(value, 10);
17
+ if (isNaN(num) || num <= 0) {
18
+ throw new Error(`Invalid max-file-size: ${value}. Must be a positive number.`);
19
+ }
20
+ return num;
21
+ }
22
+ // Severity hierarchy: critical > warning > info
23
+ const SEVERITY_LEVELS = {
24
+ critical: 3,
25
+ warning: 2,
26
+ info: 1,
27
+ };
28
+ export function severitiesAtOrAbove(minSeverity) {
29
+ const minLevel = SEVERITY_LEVELS[minSeverity];
30
+ return ['critical', 'warning', 'info'].filter((s) => SEVERITY_LEVELS[s] >= minLevel);
31
+ }
32
+ export function parseSeverities(input) {
33
+ // If single severity, treat as minimum (include this and above)
34
+ if (typeof input === 'string') {
35
+ return severitiesAtOrAbove(validateSeverity(input));
36
+ }
37
+ // If array, use exact list
38
+ return input.map(validateSeverity);
39
+ }
@@ -0,0 +1,12 @@
1
+ export interface FileEntry {
2
+ path: string;
3
+ relativePath: string;
4
+ size: number;
5
+ }
6
+ export interface WalkerOptions {
7
+ targetPath: string;
8
+ ignorePatterns: string[];
9
+ maxFileSize: number;
10
+ }
11
+ export declare function walkFiles(options: WalkerOptions): Promise<FileEntry[]>;
12
+ export declare function readFileContent(filePath: string, maxSize: number): Promise<string | null>;
@@ -0,0 +1,61 @@
1
+ import fg from 'fast-glob';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import { isBinaryBuffer } from '../utils/binary-check.js';
5
+ const DEFAULT_IGNORE = [
6
+ '**/node_modules/**',
7
+ '**/.git/**',
8
+ '**/dist/**',
9
+ '**/.next/**',
10
+ '**/build/**',
11
+ '**/coverage/**',
12
+ '**/.venv/**',
13
+ '**/__pycache__/**',
14
+ '**/vendor/**',
15
+ '**/*.min.js',
16
+ '**/*.bundle.js',
17
+ '**/*.map',
18
+ '**/package-lock.json',
19
+ '**/yarn.lock',
20
+ '**/pnpm-lock.yaml',
21
+ ];
22
+ export async function walkFiles(options) {
23
+ const { targetPath, ignorePatterns, maxFileSize } = options;
24
+ const allIgnore = [...DEFAULT_IGNORE, ...ignorePatterns];
25
+ const entries = await fg(['**/*'], {
26
+ cwd: targetPath,
27
+ ignore: allIgnore,
28
+ onlyFiles: true,
29
+ dot: false,
30
+ absolute: false,
31
+ stats: true,
32
+ });
33
+ const files = [];
34
+ for (const entry of entries) {
35
+ const stats = entry.stats;
36
+ if (!stats || stats.size > maxFileSize) {
37
+ continue;
38
+ }
39
+ files.push({
40
+ path: path.join(targetPath, entry.path),
41
+ relativePath: entry.path,
42
+ size: stats.size,
43
+ });
44
+ }
45
+ return files;
46
+ }
47
+ export async function readFileContent(filePath, maxSize) {
48
+ try {
49
+ const buffer = await fs.readFile(filePath);
50
+ if (buffer.length > maxSize) {
51
+ return null;
52
+ }
53
+ if (isBinaryBuffer(buffer)) {
54
+ return null;
55
+ }
56
+ return buffer.toString('utf-8');
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { Language, DetectionRule } from '../rules/types.js';
2
+ export declare function detectLanguages(filePath: string): Language[];
3
+ export declare function filterRulesForFile(filePath: string, allRules: DetectionRule[]): DetectionRule[];
@@ -0,0 +1,50 @@
1
+ import * as path from 'path';
2
+ const extensionMap = {
3
+ '.py': ['python'],
4
+ '.js': ['node'],
5
+ '.ts': ['node', 'typescript'],
6
+ '.tsx': ['node', 'typescript'],
7
+ '.jsx': ['node'],
8
+ '.mjs': ['node'],
9
+ '.cjs': ['node'],
10
+ '.yaml': ['yaml', 'kubernetes'],
11
+ '.yml': ['yaml', 'kubernetes'],
12
+ '.json': ['json'],
13
+ };
14
+ export function detectLanguages(filePath) {
15
+ const basename = path.basename(filePath).toLowerCase();
16
+ if (basename === 'dockerfile' || basename.endsWith('.dockerfile')) {
17
+ return ['docker'];
18
+ }
19
+ if (basename === 'docker-compose.yml' || basename === 'docker-compose.yaml') {
20
+ return ['docker', 'yaml'];
21
+ }
22
+ if (basename.includes('k8s') ||
23
+ basename.includes('kubernetes') ||
24
+ basename.includes('deployment') ||
25
+ basename.includes('service') ||
26
+ basename.includes('ingress')) {
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ if (ext === '.yml' || ext === '.yaml') {
29
+ return ['kubernetes', 'yaml'];
30
+ }
31
+ }
32
+ const ext = path.extname(filePath).toLowerCase();
33
+ return extensionMap[ext] || [];
34
+ }
35
+ export function filterRulesForFile(filePath, allRules) {
36
+ const basename = path.basename(filePath);
37
+ const languages = detectLanguages(filePath);
38
+ return allRules.filter((rule) => {
39
+ const langMatch = rule.languages.some((lang) => languages.includes(lang));
40
+ if (!langMatch)
41
+ return false;
42
+ const patternMatch = rule.filePatterns.some((pattern) => {
43
+ if (pattern.startsWith('*')) {
44
+ return basename.endsWith(pattern.slice(1));
45
+ }
46
+ return basename === pattern || basename.toLowerCase() === pattern.toLowerCase();
47
+ });
48
+ return patternMatch;
49
+ });
50
+ }
@@ -0,0 +1,10 @@
1
+ import type { ScanResult, RuleSeverity } from '../rules/types.js';
2
+ export interface ScanOptions {
3
+ targetPath: string;
4
+ ignorePatterns?: string[];
5
+ maxFileSize?: number;
6
+ includeSeverities?: RuleSeverity[];
7
+ ruleIds?: string[];
8
+ }
9
+ export declare function scan(options: ScanOptions): Promise<ScanResult>;
10
+ export { allRules } from '../rules/index.js';
@@ -0,0 +1,54 @@
1
+ import { allRules } from '../rules/index.js';
2
+ import { walkFiles, readFileContent } from './file-walker.js';
3
+ import { filterRulesForFile } from './filter.js';
4
+ import { matchRule, createFinding } from './matcher.js';
5
+ const DEFAULT_MAX_FILE_SIZE = 1024 * 1024; // 1MB
6
+ export async function scan(options) {
7
+ const startTime = Date.now();
8
+ const { targetPath, ignorePatterns = [], maxFileSize = DEFAULT_MAX_FILE_SIZE, includeSeverities = ['critical', 'warning'], ruleIds, } = options;
9
+ let rulesToRun = allRules;
10
+ if (ruleIds && ruleIds.length > 0) {
11
+ rulesToRun = allRules.filter((r) => ruleIds.includes(r.id));
12
+ }
13
+ if (includeSeverities.length > 0) {
14
+ rulesToRun = rulesToRun.filter((r) => includeSeverities.includes(r.severity));
15
+ }
16
+ const files = await walkFiles({
17
+ targetPath,
18
+ ignorePatterns,
19
+ maxFileSize,
20
+ });
21
+ const findings = [];
22
+ let scannedFiles = 0;
23
+ let skippedFiles = 0;
24
+ for (const file of files) {
25
+ const content = await readFileContent(file.path, maxFileSize);
26
+ if (content === null) {
27
+ skippedFiles++;
28
+ continue;
29
+ }
30
+ scannedFiles++;
31
+ const applicableRules = filterRulesForFile(file.relativePath, rulesToRun);
32
+ for (const rule of applicableRules) {
33
+ const matches = matchRule(content, rule);
34
+ for (const match of matches) {
35
+ findings.push(createFinding(rule, match, file.relativePath));
36
+ }
37
+ }
38
+ }
39
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length;
40
+ const warningCount = findings.filter((f) => f.severity === 'warning').length;
41
+ const infoCount = findings.filter((f) => f.severity === 'info').length;
42
+ return {
43
+ scannedFiles,
44
+ skippedFiles,
45
+ totalFindings: findings.length,
46
+ criticalCount,
47
+ warningCount,
48
+ infoCount,
49
+ findings,
50
+ duration: Date.now() - startTime,
51
+ timestamp: new Date().toISOString(),
52
+ };
53
+ }
54
+ export { allRules } from '../rules/index.js';
@@ -0,0 +1,10 @@
1
+ import type { DetectionRule, Finding } from '../rules/types.js';
2
+ export interface MatchResult {
3
+ match: string;
4
+ index: number;
5
+ line: number;
6
+ column: number;
7
+ snippet: string;
8
+ }
9
+ export declare function matchRule(content: string, rule: DetectionRule): MatchResult[];
10
+ export declare function createFinding(rule: DetectionRule, matchResult: MatchResult, relativePath: string): Finding;
@@ -0,0 +1,47 @@
1
+ import { buildLineIndex, offsetToLineCol, extractSnippet, } from '../utils/line-mapper.js';
2
+ export function matchRule(content, rule) {
3
+ const results = [];
4
+ const lineStarts = buildLineIndex(content);
5
+ const flags = rule.pattern.flags.includes('g')
6
+ ? rule.pattern.flags
7
+ : rule.pattern.flags + 'g';
8
+ const regex = new RegExp(rule.pattern.source, flags);
9
+ let match;
10
+ while ((match = regex.exec(content)) !== null) {
11
+ const matchText = match[0];
12
+ if (rule.allowPatterns?.some((p) => p.test(matchText))) {
13
+ continue;
14
+ }
15
+ const { line, column } = offsetToLineCol(match.index, lineStarts);
16
+ const snippet = extractSnippet(content, line, lineStarts);
17
+ results.push({
18
+ match: matchText,
19
+ index: match.index,
20
+ line,
21
+ column,
22
+ snippet,
23
+ });
24
+ if (match.index === regex.lastIndex) {
25
+ regex.lastIndex++;
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+ export function createFinding(rule, matchResult, relativePath) {
31
+ return {
32
+ ruleId: rule.id,
33
+ title: rule.title,
34
+ severity: rule.severity,
35
+ category: rule.category,
36
+ file: relativePath,
37
+ line: matchResult.line,
38
+ column: matchResult.column,
39
+ snippet: matchResult.snippet,
40
+ match: matchResult.match,
41
+ matchOffset: matchResult.index,
42
+ message: rule.message,
43
+ remediation: rule.remediation,
44
+ cwe: rule.cwe,
45
+ owasp: rule.owasp,
46
+ };
47
+ }