@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,65 @@
|
|
|
1
|
+
import { isFixable } from '../fix/index.js';
|
|
2
|
+
export function formatPRComment(result) {
|
|
3
|
+
const critical = result.findings.filter(f => f.severity === 'critical');
|
|
4
|
+
const warning = result.findings.filter(f => f.severity === 'warning');
|
|
5
|
+
const fixable = result.findings.filter(f => isFixable(f));
|
|
6
|
+
const sections = [];
|
|
7
|
+
// Header
|
|
8
|
+
sections.push('## 🛡️ VibeGuard Security Scan\n');
|
|
9
|
+
// Summary table
|
|
10
|
+
sections.push('| Severity | Count |');
|
|
11
|
+
sections.push('|----------|-------|');
|
|
12
|
+
sections.push(`| 🔴 Critical | ${critical.length} |`);
|
|
13
|
+
sections.push(`| 🟡 Warning | ${warning.length} |`);
|
|
14
|
+
sections.push('');
|
|
15
|
+
// Auto-fixable callout
|
|
16
|
+
if (fixable.length > 0) {
|
|
17
|
+
sections.push(`### ✅ Auto-fixable: ${fixable.length} issues`);
|
|
18
|
+
sections.push('React with 👍 to this comment to automatically apply fixes.\n');
|
|
19
|
+
}
|
|
20
|
+
// Critical issues (expanded)
|
|
21
|
+
if (critical.length > 0) {
|
|
22
|
+
sections.push('### 🔴 Critical Issues\n');
|
|
23
|
+
critical.forEach(f => {
|
|
24
|
+
sections.push(`- **${f.ruleId}** \`${f.file}:${f.line}\``);
|
|
25
|
+
sections.push(` ${f.message}`);
|
|
26
|
+
if (isFixable(f)) {
|
|
27
|
+
sections.push(` ✅ *Auto-fixable*`);
|
|
28
|
+
}
|
|
29
|
+
sections.push('');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Warning issues (collapsed)
|
|
33
|
+
if (warning.length > 0) {
|
|
34
|
+
sections.push('<details>');
|
|
35
|
+
sections.push('<summary>🟡 View warnings</summary>\n');
|
|
36
|
+
warning.forEach(f => {
|
|
37
|
+
sections.push(`- **${f.ruleId}** \`${f.file}:${f.line}\` - ${f.message}`);
|
|
38
|
+
});
|
|
39
|
+
sections.push('\n</details>\n');
|
|
40
|
+
}
|
|
41
|
+
// Footer
|
|
42
|
+
sections.push('---');
|
|
43
|
+
sections.push('<sub>Powered by [VibeGuard](https://github.com/vibeguard/vibeguard)</sub>');
|
|
44
|
+
return sections.join('\n');
|
|
45
|
+
}
|
|
46
|
+
export function formatInlineComment(finding) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(`🔴 **${finding.ruleId}**: ${finding.message}\n`);
|
|
49
|
+
if (finding.snippet) {
|
|
50
|
+
lines.push('```');
|
|
51
|
+
lines.push(finding.snippet);
|
|
52
|
+
lines.push('```\n');
|
|
53
|
+
}
|
|
54
|
+
if (finding.remediation) {
|
|
55
|
+
lines.push(`**Fix:** ${finding.remediation}\n`);
|
|
56
|
+
}
|
|
57
|
+
if (finding.cwe) {
|
|
58
|
+
const cweNum = finding.cwe.replace('CWE-', '');
|
|
59
|
+
lines.push(`[${finding.cwe}](https://cwe.mitre.org/data/definitions/${cweNum}.html)`);
|
|
60
|
+
}
|
|
61
|
+
if (isFixable(finding)) {
|
|
62
|
+
lines.push('\n✅ *This issue can be auto-fixed. React with 👍 to the PR comment to apply.*');
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { generateWorkflowYAML } from './workflow-generator.js';
|
|
5
|
+
export function installGitHubWorkflow(projectPath, options) {
|
|
6
|
+
const workflowDir = join(projectPath, '.github', 'workflows');
|
|
7
|
+
const workflowFile = join(workflowDir, 'vibeguard.yml');
|
|
8
|
+
// Create directory if needed
|
|
9
|
+
if (!existsSync(workflowDir)) {
|
|
10
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
// Check if workflow already exists
|
|
13
|
+
if (existsSync(workflowFile)) {
|
|
14
|
+
throw new Error('VibeGuard workflow already exists. Remove .github/workflows/vibeguard.yml first.');
|
|
15
|
+
}
|
|
16
|
+
// Generate workflow content
|
|
17
|
+
const workflow = generateWorkflowYAML(options);
|
|
18
|
+
// Write workflow file
|
|
19
|
+
writeFileSync(workflowFile, workflow);
|
|
20
|
+
console.log('✓ Created .github/workflows/vibeguard.yml');
|
|
21
|
+
// Try to detect repo
|
|
22
|
+
try {
|
|
23
|
+
const remoteUrl = execSync('git config --get remote.origin.url', {
|
|
24
|
+
cwd: projectPath,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
27
|
+
}).trim();
|
|
28
|
+
const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
29
|
+
if (repoMatch) {
|
|
30
|
+
console.log(`\n📋 Next steps:`);
|
|
31
|
+
console.log(` 1. Commit and push: git add .github/workflows/vibeguard.yml && git commit -m "chore: add VibeGuard workflow" && git push`);
|
|
32
|
+
console.log(` 2. Open a test PR to see VibeGuard in action`);
|
|
33
|
+
if (options.autoFix) {
|
|
34
|
+
console.log(` 3. React with 👍 to the VibeGuard comment to auto-fix issues`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
console.log('\n📋 Next steps: Commit .github/workflows/vibeguard.yml and push to trigger on PRs');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface GitHubConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
}
|
|
5
|
+
export interface InstallOptions {
|
|
6
|
+
autoFix: boolean;
|
|
7
|
+
severityThreshold: 'critical' | 'warning' | 'info';
|
|
8
|
+
aiVerify: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface PR {
|
|
11
|
+
number: number;
|
|
12
|
+
title: string;
|
|
13
|
+
state: string;
|
|
14
|
+
head: {
|
|
15
|
+
ref: string;
|
|
16
|
+
sha: string;
|
|
17
|
+
};
|
|
18
|
+
base: {
|
|
19
|
+
ref: string;
|
|
20
|
+
sha: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface PRFile {
|
|
24
|
+
filename: string;
|
|
25
|
+
status: string;
|
|
26
|
+
additions: number;
|
|
27
|
+
deletions: number;
|
|
28
|
+
changes: number;
|
|
29
|
+
patch?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ReviewInput {
|
|
32
|
+
event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT';
|
|
33
|
+
body?: string;
|
|
34
|
+
comments?: ReviewComment[];
|
|
35
|
+
}
|
|
36
|
+
export interface ReviewComment {
|
|
37
|
+
path: string;
|
|
38
|
+
line: number;
|
|
39
|
+
body: string;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function generateWorkflowYAML(options) {
|
|
2
|
+
const autoFixJob = options.autoFix ? `
|
|
3
|
+
auto-fix:
|
|
4
|
+
if: |
|
|
5
|
+
github.event_name == 'issue_comment' &&
|
|
6
|
+
github.event.issue.pull_request &&
|
|
7
|
+
contains(github.event.comment.body, '👍')
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
with:
|
|
12
|
+
ref: refs/pull/\${{ github.event.issue.number }}/head
|
|
13
|
+
token: \${{ secrets.GITHUB_TOKEN }}
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: '20'
|
|
18
|
+
|
|
19
|
+
- name: Install VibeGuard
|
|
20
|
+
run: npm install -g vibeguard
|
|
21
|
+
|
|
22
|
+
- name: Apply fixes
|
|
23
|
+
run: vibeguard fix --yes
|
|
24
|
+
|
|
25
|
+
- name: Commit fixes
|
|
26
|
+
run: |
|
|
27
|
+
git config user.name "VibeGuard Bot"
|
|
28
|
+
git config user.email "bot@vibeguard.dev"
|
|
29
|
+
git add -A
|
|
30
|
+
git commit -m "fix(security): apply VibeGuard auto-fixes" || echo "No changes"
|
|
31
|
+
git push` : '';
|
|
32
|
+
const aiVerifyFlag = options.aiVerify ? ' --ai' : '';
|
|
33
|
+
return `name: VibeGuard Security Scan
|
|
34
|
+
|
|
35
|
+
on:
|
|
36
|
+
pull_request:
|
|
37
|
+
types: [opened, synchronize, reopened]${options.autoFix ? `
|
|
38
|
+
issue_comment:
|
|
39
|
+
types: [created]` : ''}
|
|
40
|
+
|
|
41
|
+
permissions:
|
|
42
|
+
contents: ${options.autoFix ? 'write' : 'read'}
|
|
43
|
+
pull-requests: write
|
|
44
|
+
issues: write
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
scan:
|
|
48
|
+
if: github.event_name == 'pull_request'
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
with:
|
|
53
|
+
ref: \${{ github.event.pull_request.head.ref }}
|
|
54
|
+
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: '20'
|
|
58
|
+
|
|
59
|
+
- name: Install VibeGuard
|
|
60
|
+
run: npm install -g vibeguard
|
|
61
|
+
|
|
62
|
+
- name: Scan for security issues
|
|
63
|
+
id: scan
|
|
64
|
+
run: |
|
|
65
|
+
vibeguard scan --json${aiVerifyFlag} --severity ${options.severityThreshold} > scan-results.json || true
|
|
66
|
+
echo "findings=\$(jq -r '.findings | length' scan-results.json)" >> $GITHUB_OUTPUT
|
|
67
|
+
|
|
68
|
+
- name: Post PR comment
|
|
69
|
+
if: steps.scan.outputs.findings != '0'
|
|
70
|
+
uses: actions/github-script@v7
|
|
71
|
+
with:
|
|
72
|
+
script: |
|
|
73
|
+
const fs = require('fs');
|
|
74
|
+
const results = JSON.parse(fs.readFileSync('scan-results.json'));
|
|
75
|
+
|
|
76
|
+
const critical = results.findings.filter(f => f.severity === 'critical').length;
|
|
77
|
+
const warning = results.findings.filter(f => f.severity === 'warning').length;
|
|
78
|
+
const fixable = results.findings.filter(f => f.fixable).length;
|
|
79
|
+
|
|
80
|
+
const body = \`## 🛡️ VibeGuard Security Scan
|
|
81
|
+
|
|
82
|
+
| Severity | Count |
|
|
83
|
+
|----------|-------|
|
|
84
|
+
| 🔴 Critical | \${critical} |
|
|
85
|
+
| 🟡 Warning | \${warning} |
|
|
86
|
+
|
|
87
|
+
\${fixable > 0 ? \`### ✅ Auto-fixable: \${fixable} issues
|
|
88
|
+
|
|
89
|
+
React with 👍 to this comment to automatically apply fixes.
|
|
90
|
+
\` : ''}
|
|
91
|
+
<details>
|
|
92
|
+
<summary>View all findings</summary>
|
|
93
|
+
|
|
94
|
+
\${results.findings.map(f => \`- **\${f.ruleId}** \\\`\${f.file}:\${f.line}\\\` - \${f.message}\`).join('\\n')}
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
<sub>Powered by [VibeGuard](https://github.com/vibeguard/vibeguard)</sub>\`;
|
|
100
|
+
|
|
101
|
+
await github.rest.issues.createComment({
|
|
102
|
+
...context.repo,
|
|
103
|
+
issue_number: context.issue.number,
|
|
104
|
+
body
|
|
105
|
+
});
|
|
106
|
+
${autoFixJob}
|
|
107
|
+
`;
|
|
108
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScanResult } from '../rules/types.js';
|
|
2
|
+
import type { Reporter } from './types.js';
|
|
3
|
+
export declare class ConsoleReporter implements Reporter {
|
|
4
|
+
private noColor;
|
|
5
|
+
constructor(noColor?: boolean);
|
|
6
|
+
report(result: ScanResult): string;
|
|
7
|
+
private formatFinding;
|
|
8
|
+
private style;
|
|
9
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export class ConsoleReporter {
|
|
3
|
+
noColor;
|
|
4
|
+
constructor(noColor = false) {
|
|
5
|
+
this.noColor = noColor;
|
|
6
|
+
}
|
|
7
|
+
report(result) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push('');
|
|
10
|
+
lines.push(this.style(' VibeGuard v1.0.0', 'bold'));
|
|
11
|
+
lines.push('');
|
|
12
|
+
lines.push(` Scanning complete`);
|
|
13
|
+
lines.push(` Files: ${result.scannedFiles} scanned, ${result.skippedFiles} skipped`);
|
|
14
|
+
lines.push('');
|
|
15
|
+
if (result.findings.length === 0) {
|
|
16
|
+
lines.push(this.style(' ✓ No issues found', 'green'));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const sorted = [...result.findings].sort((a, b) => {
|
|
20
|
+
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
|
21
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
22
|
+
});
|
|
23
|
+
for (const finding of sorted) {
|
|
24
|
+
lines.push(this.formatFinding(finding));
|
|
25
|
+
lines.push('');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
lines.push(' ' + '─'.repeat(40));
|
|
29
|
+
lines.push(` Summary: ${this.style(String(result.criticalCount) + ' critical', 'red')}, ` +
|
|
30
|
+
`${this.style(String(result.warningCount) + ' warnings', 'yellow')}, ` +
|
|
31
|
+
`${result.infoCount} info`);
|
|
32
|
+
lines.push(` Duration: ${(result.duration / 1000).toFixed(1)}s`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
formatFinding(finding) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
const severityLabel = finding.severity.toUpperCase();
|
|
39
|
+
const severityStyled = finding.severity === 'critical'
|
|
40
|
+
? this.style(severityLabel, 'redBold')
|
|
41
|
+
: finding.severity === 'warning'
|
|
42
|
+
? this.style(severityLabel, 'yellow')
|
|
43
|
+
: this.style(severityLabel, 'blue');
|
|
44
|
+
lines.push(` ${severityStyled} ${this.style(finding.file + ':' + finding.line + ':' + finding.column, 'cyan')}`);
|
|
45
|
+
lines.push(` ${this.style(finding.ruleId, 'dim')} ${finding.title}`);
|
|
46
|
+
lines.push(` → ${finding.snippet}`);
|
|
47
|
+
if (finding.remediation) {
|
|
48
|
+
lines.push(` ${this.style('Remediation:', 'dim')} ${finding.remediation}`);
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
style(text, style) {
|
|
53
|
+
if (this.noColor)
|
|
54
|
+
return text;
|
|
55
|
+
switch (style) {
|
|
56
|
+
case 'bold':
|
|
57
|
+
return chalk.bold(text);
|
|
58
|
+
case 'red':
|
|
59
|
+
return chalk.red(text);
|
|
60
|
+
case 'redBold':
|
|
61
|
+
return chalk.red.bold(text);
|
|
62
|
+
case 'yellow':
|
|
63
|
+
return chalk.yellow(text);
|
|
64
|
+
case 'blue':
|
|
65
|
+
return chalk.blue(text);
|
|
66
|
+
case 'cyan':
|
|
67
|
+
return chalk.cyan(text);
|
|
68
|
+
case 'dim':
|
|
69
|
+
return chalk.dim(text);
|
|
70
|
+
case 'green':
|
|
71
|
+
return chalk.green(text);
|
|
72
|
+
default:
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Reporter, ReporterType } from './types.js';
|
|
2
|
+
export declare function createReporter(type: ReporterType, noColor?: boolean): Reporter;
|
|
3
|
+
export { type Reporter, type ReporterType } from './types.js';
|
|
4
|
+
export { ConsoleReporter } from './console.js';
|
|
5
|
+
export { JsonReporter } from './json.js';
|
|
6
|
+
export { SarifReporter } from './sarif.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConsoleReporter } from './console.js';
|
|
2
|
+
import { JsonReporter } from './json.js';
|
|
3
|
+
import { SarifReporter } from './sarif.js';
|
|
4
|
+
export function createReporter(type, noColor = false) {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case 'json':
|
|
7
|
+
return new JsonReporter();
|
|
8
|
+
case 'sarif':
|
|
9
|
+
return new SarifReporter();
|
|
10
|
+
case 'console':
|
|
11
|
+
default:
|
|
12
|
+
return new ConsoleReporter(noColor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export { ConsoleReporter } from './console.js';
|
|
16
|
+
export { JsonReporter } from './json.js';
|
|
17
|
+
export { SarifReporter } from './sarif.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { VERSION } from '../index.js';
|
|
2
|
+
export class JsonReporter {
|
|
3
|
+
report(result) {
|
|
4
|
+
const output = {
|
|
5
|
+
version: VERSION,
|
|
6
|
+
timestamp: result.timestamp,
|
|
7
|
+
summary: {
|
|
8
|
+
scannedFiles: result.scannedFiles,
|
|
9
|
+
skippedFiles: result.skippedFiles,
|
|
10
|
+
critical: result.criticalCount,
|
|
11
|
+
warning: result.warningCount,
|
|
12
|
+
info: result.infoCount,
|
|
13
|
+
duration: result.duration,
|
|
14
|
+
},
|
|
15
|
+
findings: result.findings.map((f) => ({
|
|
16
|
+
ruleId: f.ruleId,
|
|
17
|
+
title: f.title,
|
|
18
|
+
severity: f.severity,
|
|
19
|
+
category: f.category,
|
|
20
|
+
file: f.file,
|
|
21
|
+
line: f.line,
|
|
22
|
+
column: f.column,
|
|
23
|
+
snippet: f.snippet,
|
|
24
|
+
message: f.message,
|
|
25
|
+
remediation: f.remediation,
|
|
26
|
+
cwe: f.cwe,
|
|
27
|
+
owasp: f.owasp,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
return JSON.stringify(output, null, 2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScanResult } from '../rules/types.js';
|
|
2
|
+
import type { Reporter } from './types.js';
|
|
3
|
+
export declare class SarifReporter implements Reporter {
|
|
4
|
+
report(result: ScanResult): string;
|
|
5
|
+
private buildRules;
|
|
6
|
+
private buildResults;
|
|
7
|
+
private mapSeverity;
|
|
8
|
+
private getSeverityScore;
|
|
9
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { VERSION } from '../index.js';
|
|
2
|
+
import { allRules } from '../rules/index.js';
|
|
3
|
+
export class SarifReporter {
|
|
4
|
+
report(result) {
|
|
5
|
+
const rules = this.buildRules(result.findings);
|
|
6
|
+
const results = this.buildResults(result.findings);
|
|
7
|
+
const sarif = {
|
|
8
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
9
|
+
version: '2.1.0',
|
|
10
|
+
runs: [
|
|
11
|
+
{
|
|
12
|
+
tool: {
|
|
13
|
+
driver: {
|
|
14
|
+
name: 'VibeGuard',
|
|
15
|
+
version: VERSION,
|
|
16
|
+
informationUri: 'https://github.com/vibeguard/vibeguard',
|
|
17
|
+
rules,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
results,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
return JSON.stringify(sarif, null, 2);
|
|
25
|
+
}
|
|
26
|
+
buildRules(findings) {
|
|
27
|
+
const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
|
|
28
|
+
return ruleIds.map((id) => {
|
|
29
|
+
const rule = allRules.find((r) => r.id === id);
|
|
30
|
+
const finding = findings.find((f) => f.ruleId === id);
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
name: rule?.title || finding?.title || id,
|
|
34
|
+
shortDescription: { text: rule?.message || finding?.message || '' },
|
|
35
|
+
fullDescription: rule?.remediation ? { text: rule.remediation } : undefined,
|
|
36
|
+
properties: {
|
|
37
|
+
security_severity: this.getSeverityScore(finding?.severity || 'info'),
|
|
38
|
+
tags: rule?.cwe ? [rule.cwe] : [],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
buildResults(findings) {
|
|
44
|
+
return findings.map((f) => ({
|
|
45
|
+
ruleId: f.ruleId,
|
|
46
|
+
level: this.mapSeverity(f.severity),
|
|
47
|
+
message: { text: `${f.message}\n\nSnippet: ${f.snippet}` },
|
|
48
|
+
locations: [
|
|
49
|
+
{
|
|
50
|
+
physicalLocation: {
|
|
51
|
+
artifactLocation: { uri: f.file },
|
|
52
|
+
region: { startLine: f.line, startColumn: f.column },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
mapSeverity(severity) {
|
|
59
|
+
switch (severity) {
|
|
60
|
+
case 'critical':
|
|
61
|
+
return 'error';
|
|
62
|
+
case 'warning':
|
|
63
|
+
return 'warning';
|
|
64
|
+
default:
|
|
65
|
+
return 'note';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getSeverityScore(severity) {
|
|
69
|
+
switch (severity) {
|
|
70
|
+
case 'critical':
|
|
71
|
+
return '9.0';
|
|
72
|
+
case 'warning':
|
|
73
|
+
return '6.0';
|
|
74
|
+
default:
|
|
75
|
+
return '3.0';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const configRules = [
|
|
2
|
+
{
|
|
3
|
+
id: 'VG-CFG-001',
|
|
4
|
+
title: 'Service Bound to 0.0.0.0',
|
|
5
|
+
severity: 'warning',
|
|
6
|
+
category: 'config',
|
|
7
|
+
languages: ['node', 'typescript', 'python'],
|
|
8
|
+
filePatterns: ['*.js', '*.ts', '*.mjs', '*.cjs', '*.py'],
|
|
9
|
+
pattern: /(?:host\s*[:=]\s*['"]0\.0\.0\.0['"]|\.listen\s*\([^)]*['"]0\.0\.0\.0['"])/g,
|
|
10
|
+
message: 'Service binds to all interfaces. May expose internal services externally.',
|
|
11
|
+
remediation: 'Bind to 127.0.0.1 for local-only services. Use reverse proxy for external access.',
|
|
12
|
+
confidence: 'medium',
|
|
13
|
+
cwe: 'CWE-668',
|
|
14
|
+
owasp: 'A05:2021',
|
|
15
|
+
aiVerification: { enabled: true },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'VG-CFG-002',
|
|
19
|
+
title: 'Public S3 Bucket ACL',
|
|
20
|
+
severity: 'critical',
|
|
21
|
+
category: 'config',
|
|
22
|
+
languages: ['node', 'typescript', 'python', 'yaml', 'json'],
|
|
23
|
+
filePatterns: ['*.js', '*.ts', '*.py', '*.yml', '*.yaml', '*.json', '*.tf'],
|
|
24
|
+
pattern: /(?:ACL['":\s]+public-read|"Principal"\s*:\s*"\*")/g,
|
|
25
|
+
message: 'S3 bucket configured for public access. Data exposed to internet.',
|
|
26
|
+
remediation: 'Remove public-read ACL. Use private buckets with signed URLs.',
|
|
27
|
+
confidence: 'high',
|
|
28
|
+
cwe: 'CWE-732',
|
|
29
|
+
owasp: 'A01:2021',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const dependencyRules = [
|
|
2
|
+
{
|
|
3
|
+
id: 'VG-DEP-001',
|
|
4
|
+
title: 'Ghost Dependency (PyPI)',
|
|
5
|
+
severity: 'critical',
|
|
6
|
+
category: 'dependency',
|
|
7
|
+
languages: ['python'],
|
|
8
|
+
filePatterns: ['*.py', 'requirements.txt', 'Pipfile', 'pyproject.toml'],
|
|
9
|
+
pattern: /(?:from|import)\s+(secure[-_]|enterprise[-_]|flask[-_]admin[-_]|langchain[a-z]+)/g,
|
|
10
|
+
message: 'Potentially hallucinated package name. Attacker may have registered malicious package.',
|
|
11
|
+
remediation: 'Verify package exists on PyPI. Check package ownership and downloads.',
|
|
12
|
+
confidence: 'medium',
|
|
13
|
+
cwe: 'CWE-829',
|
|
14
|
+
owasp: 'A06:2021',
|
|
15
|
+
aiVerification: { enabled: true },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'VG-DEP-002',
|
|
19
|
+
title: 'Ghost Dependency (npm)',
|
|
20
|
+
severity: 'critical',
|
|
21
|
+
category: 'dependency',
|
|
22
|
+
languages: ['node', 'typescript'],
|
|
23
|
+
filePatterns: ['*.js', '*.ts', '*.mjs', '*.cjs', 'package.json'],
|
|
24
|
+
pattern: /(?:require|import)\s*\(?['"](?:huggingface[-_]|@enterprise\/|react[-_]native[-_]toolkit)/g,
|
|
25
|
+
message: 'Potentially hallucinated package name. Attacker may have registered malicious package.',
|
|
26
|
+
remediation: 'Verify package exists on npm. Check package ownership and weekly downloads.',
|
|
27
|
+
confidence: 'medium',
|
|
28
|
+
cwe: 'CWE-829',
|
|
29
|
+
owasp: 'A06:2021',
|
|
30
|
+
aiVerification: { enabled: true },
|
|
31
|
+
},
|
|
32
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const dockerRules = [
|
|
2
|
+
{
|
|
3
|
+
id: 'VG-DOCK-001',
|
|
4
|
+
title: 'Container Running as Root',
|
|
5
|
+
severity: 'warning',
|
|
6
|
+
category: 'docker',
|
|
7
|
+
languages: ['docker'],
|
|
8
|
+
filePatterns: ['Dockerfile', '*.dockerfile', 'Dockerfile.*'],
|
|
9
|
+
pattern: /^(?!.*\bUSER\b).*$/gm,
|
|
10
|
+
message: 'Dockerfile has no USER directive. Container runs as root by default.',
|
|
11
|
+
remediation: 'Add USER directive to run as non-root. Example: USER 1000:1000',
|
|
12
|
+
confidence: 'medium',
|
|
13
|
+
cwe: 'CWE-250',
|
|
14
|
+
owasp: 'A05:2021',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'VG-DOCK-002',
|
|
18
|
+
title: 'Docker Socket Exposed',
|
|
19
|
+
severity: 'critical',
|
|
20
|
+
category: 'docker',
|
|
21
|
+
languages: ['docker', 'yaml'],
|
|
22
|
+
filePatterns: ['docker-compose.yml', 'docker-compose.yaml', '*.yml', '*.yaml'],
|
|
23
|
+
pattern: /\/var\/run\/docker\.sock/g,
|
|
24
|
+
message: 'Docker socket mounted into container. Grants full host control.',
|
|
25
|
+
remediation: 'Remove docker.sock mount. Use Docker API over TCP with TLS if needed.',
|
|
26
|
+
confidence: 'high',
|
|
27
|
+
cwe: 'CWE-269',
|
|
28
|
+
owasp: 'A05:2021',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'VG-DOCK-003',
|
|
32
|
+
title: 'Privileged Container',
|
|
33
|
+
severity: 'critical',
|
|
34
|
+
category: 'docker',
|
|
35
|
+
languages: ['docker', 'yaml', 'kubernetes'],
|
|
36
|
+
filePatterns: ['docker-compose.yml', 'docker-compose.yaml', '*.yml', '*.yaml'],
|
|
37
|
+
pattern: /(?:--privileged|privileged\s*:\s*true)/g,
|
|
38
|
+
message: 'Privileged container has full host access. Attacker can escape container.',
|
|
39
|
+
remediation: 'Remove privileged flag. Use specific capabilities if needed.',
|
|
40
|
+
confidence: 'high',
|
|
41
|
+
cwe: 'CWE-250',
|
|
42
|
+
owasp: 'A05:2021',
|
|
43
|
+
},
|
|
44
|
+
];
|