@oculum/scanner 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/dist/formatters/cli-terminal.d.ts +27 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -0
- package/dist/formatters/cli-terminal.js +412 -0
- package/dist/formatters/cli-terminal.js.map +1 -0
- package/dist/formatters/github-comment.d.ts +41 -0
- package/dist/formatters/github-comment.d.ts.map +1 -0
- package/dist/formatters/github-comment.js +306 -0
- package/dist/formatters/github-comment.js.map +1 -0
- package/dist/formatters/grouping.d.ts +52 -0
- package/dist/formatters/grouping.d.ts.map +1 -0
- package/dist/formatters/grouping.js +152 -0
- package/dist/formatters/grouping.js.map +1 -0
- package/dist/formatters/index.d.ts +9 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +35 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/vscode-diagnostic.d.ts +103 -0
- package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
- package/dist/formatters/vscode-diagnostic.js +151 -0
- package/dist/formatters/vscode-diagnostic.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/layer1/comments.d.ts +8 -0
- package/dist/layer1/comments.d.ts.map +1 -0
- package/dist/layer1/comments.js +203 -0
- package/dist/layer1/comments.js.map +1 -0
- package/dist/layer1/config-audit.d.ts +8 -0
- package/dist/layer1/config-audit.d.ts.map +1 -0
- package/dist/layer1/config-audit.js +252 -0
- package/dist/layer1/config-audit.js.map +1 -0
- package/dist/layer1/entropy.d.ts +8 -0
- package/dist/layer1/entropy.d.ts.map +1 -0
- package/dist/layer1/entropy.js +500 -0
- package/dist/layer1/entropy.js.map +1 -0
- package/dist/layer1/file-flags.d.ts +7 -0
- package/dist/layer1/file-flags.d.ts.map +1 -0
- package/dist/layer1/file-flags.js +112 -0
- package/dist/layer1/file-flags.js.map +1 -0
- package/dist/layer1/index.d.ts +36 -0
- package/dist/layer1/index.d.ts.map +1 -0
- package/dist/layer1/index.js +132 -0
- package/dist/layer1/index.js.map +1 -0
- package/dist/layer1/patterns.d.ts +8 -0
- package/dist/layer1/patterns.d.ts.map +1 -0
- package/dist/layer1/patterns.js +482 -0
- package/dist/layer1/patterns.js.map +1 -0
- package/dist/layer1/urls.d.ts +8 -0
- package/dist/layer1/urls.d.ts.map +1 -0
- package/dist/layer1/urls.js +296 -0
- package/dist/layer1/urls.js.map +1 -0
- package/dist/layer1/weak-crypto.d.ts +7 -0
- package/dist/layer1/weak-crypto.d.ts.map +1 -0
- package/dist/layer1/weak-crypto.js +291 -0
- package/dist/layer1/weak-crypto.js.map +1 -0
- package/dist/layer2/ai-agent-tools.d.ts +19 -0
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
- package/dist/layer2/ai-agent-tools.js +528 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -0
- package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
- package/dist/layer2/ai-endpoint-protection.js +332 -0
- package/dist/layer2/ai-endpoint-protection.js.map +1 -0
- package/dist/layer2/ai-execution-sinks.d.ts +18 -0
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
- package/dist/layer2/ai-execution-sinks.js +496 -0
- package/dist/layer2/ai-execution-sinks.js.map +1 -0
- package/dist/layer2/ai-fingerprinting.d.ts +7 -0
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
- package/dist/layer2/ai-fingerprinting.js +654 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.js +356 -0
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
- package/dist/layer2/ai-rag-safety.d.ts +21 -0
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
- package/dist/layer2/ai-rag-safety.js +459 -0
- package/dist/layer2/ai-rag-safety.js.map +1 -0
- package/dist/layer2/ai-schema-validation.d.ts +25 -0
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
- package/dist/layer2/ai-schema-validation.js +375 -0
- package/dist/layer2/ai-schema-validation.js.map +1 -0
- package/dist/layer2/auth-antipatterns.d.ts +20 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
- package/dist/layer2/auth-antipatterns.js +333 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -0
- package/dist/layer2/byok-patterns.d.ts +12 -0
- package/dist/layer2/byok-patterns.d.ts.map +1 -0
- package/dist/layer2/byok-patterns.js +299 -0
- package/dist/layer2/byok-patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions.d.ts +7 -0
- package/dist/layer2/dangerous-functions.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions.js +1375 -0
- package/dist/layer2/dangerous-functions.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts +16 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -0
- package/dist/layer2/data-exposure.js +279 -0
- package/dist/layer2/data-exposure.js.map +1 -0
- package/dist/layer2/framework-checks.d.ts +7 -0
- package/dist/layer2/framework-checks.d.ts.map +1 -0
- package/dist/layer2/framework-checks.js +388 -0
- package/dist/layer2/framework-checks.js.map +1 -0
- package/dist/layer2/index.d.ts +58 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +380 -0
- package/dist/layer2/index.js.map +1 -0
- package/dist/layer2/logic-gates.d.ts +7 -0
- package/dist/layer2/logic-gates.d.ts.map +1 -0
- package/dist/layer2/logic-gates.js +182 -0
- package/dist/layer2/logic-gates.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts +7 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -0
- package/dist/layer2/risky-imports.js +161 -0
- package/dist/layer2/risky-imports.js.map +1 -0
- package/dist/layer2/variables.d.ts +8 -0
- package/dist/layer2/variables.d.ts.map +1 -0
- package/dist/layer2/variables.js +152 -0
- package/dist/layer2/variables.js.map +1 -0
- package/dist/layer3/anthropic.d.ts +83 -0
- package/dist/layer3/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic.js +1745 -0
- package/dist/layer3/anthropic.js.map +1 -0
- package/dist/layer3/index.d.ts +24 -0
- package/dist/layer3/index.d.ts.map +1 -0
- package/dist/layer3/index.js +119 -0
- package/dist/layer3/index.js.map +1 -0
- package/dist/layer3/openai.d.ts +25 -0
- package/dist/layer3/openai.d.ts.map +1 -0
- package/dist/layer3/openai.js +238 -0
- package/dist/layer3/openai.js.map +1 -0
- package/dist/layer3/package-check.d.ts +63 -0
- package/dist/layer3/package-check.d.ts.map +1 -0
- package/dist/layer3/package-check.js +508 -0
- package/dist/layer3/package-check.js.map +1 -0
- package/dist/modes/incremental.d.ts +66 -0
- package/dist/modes/incremental.d.ts.map +1 -0
- package/dist/modes/incremental.js +200 -0
- package/dist/modes/incremental.js.map +1 -0
- package/dist/tiers.d.ts +125 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +234 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth-helper-detector.d.ts +56 -0
- package/dist/utils/auth-helper-detector.d.ts.map +1 -0
- package/dist/utils/auth-helper-detector.js +360 -0
- package/dist/utils/auth-helper-detector.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +96 -0
- package/dist/utils/context-helpers.d.ts.map +1 -0
- package/dist/utils/context-helpers.js +493 -0
- package/dist/utils/context-helpers.js.map +1 -0
- package/dist/utils/diff-detector.d.ts +53 -0
- package/dist/utils/diff-detector.d.ts.map +1 -0
- package/dist/utils/diff-detector.js +104 -0
- package/dist/utils/diff-detector.js.map +1 -0
- package/dist/utils/diff-parser.d.ts +80 -0
- package/dist/utils/diff-parser.d.ts.map +1 -0
- package/dist/utils/diff-parser.js +202 -0
- package/dist/utils/diff-parser.js.map +1 -0
- package/dist/utils/imported-auth-detector.d.ts +37 -0
- package/dist/utils/imported-auth-detector.d.ts.map +1 -0
- package/dist/utils/imported-auth-detector.js +251 -0
- package/dist/utils/imported-auth-detector.js.map +1 -0
- package/dist/utils/middleware-detector.d.ts +55 -0
- package/dist/utils/middleware-detector.d.ts.map +1 -0
- package/dist/utils/middleware-detector.js +260 -0
- package/dist/utils/middleware-detector.js.map +1 -0
- package/dist/utils/oauth-flow-detector.d.ts +41 -0
- package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
- package/dist/utils/oauth-flow-detector.js +202 -0
- package/dist/utils/oauth-flow-detector.js.map +1 -0
- package/dist/utils/path-exclusions.d.ts +55 -0
- package/dist/utils/path-exclusions.d.ts.map +1 -0
- package/dist/utils/path-exclusions.js +222 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/project-context-builder.d.ts +119 -0
- package/dist/utils/project-context-builder.d.ts.map +1 -0
- package/dist/utils/project-context-builder.js +534 -0
- package/dist/utils/project-context-builder.js.map +1 -0
- package/dist/utils/registry-clients.d.ts +93 -0
- package/dist/utils/registry-clients.d.ts.map +1 -0
- package/dist/utils/registry-clients.js +273 -0
- package/dist/utils/registry-clients.js.map +1 -0
- package/dist/utils/trpc-analyzer.d.ts +78 -0
- package/dist/utils/trpc-analyzer.d.ts.map +1 -0
- package/dist/utils/trpc-analyzer.js +297 -0
- package/dist/utils/trpc-analyzer.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
- package/src/__tests__/benchmark/fixtures/index.ts +68 -0
- package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
- package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
- package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
- package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
- package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
- package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
- package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
- package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
- package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
- package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
- package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
- package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
- package/src/__tests__/benchmark/index.ts +29 -0
- package/src/__tests__/benchmark/run-benchmark.ts +144 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
- package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
- package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
- package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
- package/src/__tests__/benchmark/types.ts +144 -0
- package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
- package/src/__tests__/regression/known-false-positives.test.ts +467 -0
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
- package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
- package/src/__tests__/validation/analyze-results.ts +542 -0
- package/src/__tests__/validation/extract-for-triage.ts +146 -0
- package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
- package/src/__tests__/validation/run-validation.ts +364 -0
- package/src/__tests__/validation/triage-template.md +132 -0
- package/src/formatters/cli-terminal.ts +446 -0
- package/src/formatters/github-comment.ts +382 -0
- package/src/formatters/grouping.ts +190 -0
- package/src/formatters/index.ts +47 -0
- package/src/formatters/vscode-diagnostic.ts +243 -0
- package/src/index.ts +823 -0
- package/src/layer1/comments.ts +218 -0
- package/src/layer1/config-audit.ts +289 -0
- package/src/layer1/entropy.ts +583 -0
- package/src/layer1/file-flags.ts +127 -0
- package/src/layer1/index.ts +181 -0
- package/src/layer1/patterns.ts +516 -0
- package/src/layer1/urls.ts +334 -0
- package/src/layer1/weak-crypto.ts +328 -0
- package/src/layer2/ai-agent-tools.ts +601 -0
- package/src/layer2/ai-endpoint-protection.ts +387 -0
- package/src/layer2/ai-execution-sinks.ts +580 -0
- package/src/layer2/ai-fingerprinting.ts +758 -0
- package/src/layer2/ai-prompt-hygiene.ts +411 -0
- package/src/layer2/ai-rag-safety.ts +511 -0
- package/src/layer2/ai-schema-validation.ts +421 -0
- package/src/layer2/auth-antipatterns.ts +394 -0
- package/src/layer2/byok-patterns.ts +336 -0
- package/src/layer2/dangerous-functions.ts +1563 -0
- package/src/layer2/data-exposure.ts +315 -0
- package/src/layer2/framework-checks.ts +433 -0
- package/src/layer2/index.ts +473 -0
- package/src/layer2/logic-gates.ts +206 -0
- package/src/layer2/risky-imports.ts +186 -0
- package/src/layer2/variables.ts +166 -0
- package/src/layer3/anthropic.ts +2030 -0
- package/src/layer3/index.ts +130 -0
- package/src/layer3/package-check.ts +604 -0
- package/src/modes/incremental.ts +293 -0
- package/src/tiers.ts +318 -0
- package/src/types.ts +284 -0
- package/src/utils/auth-helper-detector.ts +443 -0
- package/src/utils/context-helpers.ts +535 -0
- package/src/utils/diff-detector.ts +135 -0
- package/src/utils/diff-parser.ts +272 -0
- package/src/utils/imported-auth-detector.ts +320 -0
- package/src/utils/middleware-detector.ts +333 -0
- package/src/utils/oauth-flow-detector.ts +246 -0
- package/src/utils/path-exclusions.ts +266 -0
- package/src/utils/project-context-builder.ts +707 -0
- package/src/utils/registry-clients.ts +351 -0
- package/src/utils/trpc-analyzer.ts +382 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Comment Formatter
|
|
3
|
+
* Formats scan results as markdown for GitHub PR comments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ScanResult, Vulnerability, VulnerabilitySeverity, VulnerabilityCategory } from '../types'
|
|
7
|
+
import { groupByTheme, limitPerGroup, getBlockingIssues, GroupedFindings } from './grouping'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Severity badges for GitHub markdown
|
|
11
|
+
*/
|
|
12
|
+
const SEVERITY_BADGE: Record<VulnerabilitySeverity, string> = {
|
|
13
|
+
critical: '🔴 Critical',
|
|
14
|
+
high: '🟠 High',
|
|
15
|
+
medium: '🟡 Medium',
|
|
16
|
+
low: '🔵 Low',
|
|
17
|
+
info: '⚪ Info',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Category documentation URLs
|
|
22
|
+
*/
|
|
23
|
+
const CATEGORY_DOCS: Partial<Record<VulnerabilityCategory, string>> = {
|
|
24
|
+
hardcoded_secret: 'https://oculum.dev/docs/rules/hardcoded-secrets',
|
|
25
|
+
ai_prompt_injection: 'https://oculum.dev/docs/rules/prompt-injection',
|
|
26
|
+
ai_unsafe_execution: 'https://oculum.dev/docs/rules/unsafe-execution',
|
|
27
|
+
ai_overpermissive_tool: 'https://oculum.dev/docs/rules/overpermissive-tools',
|
|
28
|
+
ai_rag_exfiltration: 'https://oculum.dev/docs/rules/rag-exfiltration',
|
|
29
|
+
ai_endpoint_unprotected: 'https://oculum.dev/docs/rules/unprotected-endpoints',
|
|
30
|
+
ai_schema_mismatch: 'https://oculum.dev/docs/rules/schema-validation',
|
|
31
|
+
sql_injection: 'https://oculum.dev/docs/rules/sql-injection',
|
|
32
|
+
xss: 'https://oculum.dev/docs/rules/xss',
|
|
33
|
+
missing_auth: 'https://oculum.dev/docs/rules/missing-auth',
|
|
34
|
+
data_exposure: 'https://oculum.dev/docs/rules/data-exposure',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a single finding as a markdown list item
|
|
39
|
+
*/
|
|
40
|
+
function formatFinding(finding: Vulnerability, options: { showFile?: boolean; showDocs?: boolean } = {}): string {
|
|
41
|
+
const { showFile = true, showDocs = true } = options
|
|
42
|
+
const badge = SEVERITY_BADGE[finding.severity]
|
|
43
|
+
const location = showFile
|
|
44
|
+
? `\`${finding.filePath}:${finding.lineNumber}\``
|
|
45
|
+
: `Line ${finding.lineNumber}`
|
|
46
|
+
|
|
47
|
+
let md = `- ${badge} **${finding.title}**\n`
|
|
48
|
+
md += ` - 📍 ${location}\n`
|
|
49
|
+
md += ` - ${finding.description}\n`
|
|
50
|
+
|
|
51
|
+
if (finding.suggestedFix) {
|
|
52
|
+
md += ` - 💡 **Fix:** ${finding.suggestedFix}\n`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add documentation link if available
|
|
56
|
+
const docsUrl = CATEGORY_DOCS[finding.category]
|
|
57
|
+
if (showDocs && docsUrl) {
|
|
58
|
+
md += ` - 📚 [Learn more](${docsUrl})\n`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return md
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a group of findings
|
|
66
|
+
*/
|
|
67
|
+
function formatGroup(group: GroupedFindings, maxFindings: number = 5): string {
|
|
68
|
+
const { themeIcon, themeName, findings, severityCounts } = group
|
|
69
|
+
|
|
70
|
+
// Count summary
|
|
71
|
+
const counts: string[] = []
|
|
72
|
+
if (severityCounts.critical > 0) counts.push(`${severityCounts.critical} critical`)
|
|
73
|
+
if (severityCounts.high > 0) counts.push(`${severityCounts.high} high`)
|
|
74
|
+
if (severityCounts.medium > 0) counts.push(`${severityCounts.medium} medium`)
|
|
75
|
+
if (severityCounts.low > 0) counts.push(`${severityCounts.low} low`)
|
|
76
|
+
if (severityCounts.info > 0) counts.push(`${severityCounts.info} info`)
|
|
77
|
+
|
|
78
|
+
let md = `### ${themeIcon} ${themeName}\n`
|
|
79
|
+
md += `> ${counts.join(', ')}\n\n`
|
|
80
|
+
|
|
81
|
+
// Show top findings
|
|
82
|
+
const shown = findings.slice(0, maxFindings)
|
|
83
|
+
for (const finding of shown) {
|
|
84
|
+
md += formatFinding(finding) + '\n'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show truncation notice if needed
|
|
88
|
+
if (findings.length > maxFindings) {
|
|
89
|
+
md += `<details>\n<summary>+ ${findings.length - maxFindings} more ${themeName.toLowerCase()} issues</summary>\n\n`
|
|
90
|
+
for (const finding of findings.slice(maxFindings)) {
|
|
91
|
+
md += formatFinding(finding) + '\n'
|
|
92
|
+
}
|
|
93
|
+
md += `</details>\n`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return md
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Options for GitHub comment formatting
|
|
101
|
+
*/
|
|
102
|
+
export interface GitHubCommentOptions {
|
|
103
|
+
maxFindingsPerGroup?: number
|
|
104
|
+
showAllFindings?: boolean
|
|
105
|
+
includeFooter?: boolean
|
|
106
|
+
scanDepth?: 'cheap' | 'validated' | 'deep'
|
|
107
|
+
previousScanCounts?: {
|
|
108
|
+
critical: number
|
|
109
|
+
high: number
|
|
110
|
+
medium: number
|
|
111
|
+
low: number
|
|
112
|
+
info: number
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format scan result as GitHub PR comment
|
|
118
|
+
*/
|
|
119
|
+
export function formatGitHubComment(result: ScanResult, options: GitHubCommentOptions = {}): string {
|
|
120
|
+
const {
|
|
121
|
+
maxFindingsPerGroup = 5,
|
|
122
|
+
showAllFindings = false,
|
|
123
|
+
includeFooter = true,
|
|
124
|
+
scanDepth,
|
|
125
|
+
previousScanCounts,
|
|
126
|
+
} = options
|
|
127
|
+
|
|
128
|
+
const { vulnerabilities, severityCounts, hasBlockingIssues } = result
|
|
129
|
+
|
|
130
|
+
// Professional header with Oculum branding
|
|
131
|
+
let md = `<!-- oculum-security-scan -->\n`
|
|
132
|
+
md += `<div align="center">\n\n`
|
|
133
|
+
md += `# 🛡️ Oculum Security Scan\n\n`
|
|
134
|
+
md += `</div>\n\n`
|
|
135
|
+
|
|
136
|
+
// Status banner
|
|
137
|
+
if (hasBlockingIssues) {
|
|
138
|
+
const blocking = severityCounts.critical + severityCounts.high
|
|
139
|
+
md += `> 🚨 **${blocking} blocking issue${blocking === 1 ? '' : 's'} found** — These must be addressed before merging.\n\n`
|
|
140
|
+
} else if (vulnerabilities.length > 0) {
|
|
141
|
+
md += `> ⚠️ **${vulnerabilities.length} issue${vulnerabilities.length === 1 ? '' : 's'} found** — Review recommended, but no blocking issues.\n\n`
|
|
142
|
+
} else {
|
|
143
|
+
md += `> ✅ **No security issues detected** — This PR looks good!\n\n`
|
|
144
|
+
md += formatScanMetadata(result, scanDepth)
|
|
145
|
+
if (includeFooter) {
|
|
146
|
+
md += formatFooter()
|
|
147
|
+
}
|
|
148
|
+
return md
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Comparison with previous scan if available
|
|
152
|
+
if (previousScanCounts) {
|
|
153
|
+
md += formatComparison(severityCounts, previousScanCounts)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Summary section
|
|
157
|
+
md += `## 📊 Summary\n\n`
|
|
158
|
+
md += formatSummaryTable(severityCounts)
|
|
159
|
+
md += '\n'
|
|
160
|
+
|
|
161
|
+
// Scan metadata
|
|
162
|
+
md += formatScanMetadata(result, scanDepth)
|
|
163
|
+
|
|
164
|
+
// Blocking issues section (critical + high)
|
|
165
|
+
const blockingIssues = getBlockingIssues(vulnerabilities)
|
|
166
|
+
if (blockingIssues.length > 0) {
|
|
167
|
+
md += `## 🚨 Blocking Issues\n\n`
|
|
168
|
+
md += `> These issues must be fixed before merging.\n\n`
|
|
169
|
+
|
|
170
|
+
for (const finding of blockingIssues.slice(0, 10)) {
|
|
171
|
+
md += formatFinding(finding) + '\n'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (blockingIssues.length > 10) {
|
|
175
|
+
md += `<details>\n<summary>+ ${blockingIssues.length - 10} more blocking issues</summary>\n\n`
|
|
176
|
+
for (const finding of blockingIssues.slice(10)) {
|
|
177
|
+
md += formatFinding(finding, { showDocs: false }) + '\n'
|
|
178
|
+
}
|
|
179
|
+
md += `</details>\n`
|
|
180
|
+
}
|
|
181
|
+
md += '\n'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// All findings grouped by theme
|
|
185
|
+
const grouped = groupByTheme(vulnerabilities)
|
|
186
|
+
const limited = showAllFindings ? grouped : limitPerGroup(grouped, 10)
|
|
187
|
+
|
|
188
|
+
// Only show non-blocking findings in "All Findings" section
|
|
189
|
+
const hasNonBlockingFindings = vulnerabilities.some(
|
|
190
|
+
v => v.severity !== 'critical' && v.severity !== 'high'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if (hasNonBlockingFindings) {
|
|
194
|
+
md += `## 📋 All Findings by Category\n\n`
|
|
195
|
+
|
|
196
|
+
for (const group of limited) {
|
|
197
|
+
// Skip groups with only blocking issues (already shown above)
|
|
198
|
+
const nonBlockingInGroup = group.findings.filter(
|
|
199
|
+
f => f.severity !== 'critical' && f.severity !== 'high'
|
|
200
|
+
)
|
|
201
|
+
if (nonBlockingInGroup.length === 0) continue
|
|
202
|
+
|
|
203
|
+
md += formatGroup(group, maxFindingsPerGroup) + '\n'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Quick actions
|
|
208
|
+
md += formatQuickActions(hasBlockingIssues)
|
|
209
|
+
|
|
210
|
+
// Footer
|
|
211
|
+
if (includeFooter) {
|
|
212
|
+
md += formatFooter()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return md
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Format summary table
|
|
220
|
+
*/
|
|
221
|
+
function formatSummaryTable(severityCounts: Record<VulnerabilitySeverity, number>): string {
|
|
222
|
+
let md = `| Severity | Count | Status |\n`
|
|
223
|
+
md += `|:---------|:-----:|:------:|\n`
|
|
224
|
+
|
|
225
|
+
if (severityCounts.critical > 0) {
|
|
226
|
+
md += `| 🔴 **Critical** | ${severityCounts.critical} | ❌ Blocking |\n`
|
|
227
|
+
}
|
|
228
|
+
if (severityCounts.high > 0) {
|
|
229
|
+
md += `| 🟠 **High** | ${severityCounts.high} | ❌ Blocking |\n`
|
|
230
|
+
}
|
|
231
|
+
if (severityCounts.medium > 0) {
|
|
232
|
+
md += `| 🟡 Medium | ${severityCounts.medium} | ⚠️ Review |\n`
|
|
233
|
+
}
|
|
234
|
+
if (severityCounts.low > 0) {
|
|
235
|
+
md += `| 🔵 Low | ${severityCounts.low} | ℹ️ Info |\n`
|
|
236
|
+
}
|
|
237
|
+
if (severityCounts.info > 0) {
|
|
238
|
+
md += `| ⚪ Info | ${severityCounts.info} | ℹ️ Info |\n`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return md
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Format comparison with previous scan
|
|
246
|
+
*/
|
|
247
|
+
function formatComparison(
|
|
248
|
+
current: Record<VulnerabilitySeverity, number>,
|
|
249
|
+
previous: Record<VulnerabilitySeverity, number>
|
|
250
|
+
): string {
|
|
251
|
+
const currentTotal = Object.values(current).reduce((a, b) => a + b, 0)
|
|
252
|
+
const previousTotal = Object.values(previous).reduce((a, b) => a + b, 0)
|
|
253
|
+
const diff = currentTotal - previousTotal
|
|
254
|
+
|
|
255
|
+
const currentBlocking = current.critical + current.high
|
|
256
|
+
const previousBlocking = previous.critical + previous.high
|
|
257
|
+
const blockingDiff = currentBlocking - previousBlocking
|
|
258
|
+
|
|
259
|
+
let md = `### 📈 Changes from Previous Scan\n\n`
|
|
260
|
+
|
|
261
|
+
if (diff === 0 && blockingDiff === 0) {
|
|
262
|
+
md += `No change in findings.\n\n`
|
|
263
|
+
} else {
|
|
264
|
+
const parts: string[] = []
|
|
265
|
+
|
|
266
|
+
if (blockingDiff > 0) {
|
|
267
|
+
parts.push(`🔺 **${blockingDiff} new blocking issue${blockingDiff === 1 ? '' : 's'}**`)
|
|
268
|
+
} else if (blockingDiff < 0) {
|
|
269
|
+
parts.push(`🔻 **${Math.abs(blockingDiff)} blocking issue${Math.abs(blockingDiff) === 1 ? '' : 's'} resolved**`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (diff > 0 && diff !== blockingDiff) {
|
|
273
|
+
parts.push(`${diff - blockingDiff} new non-blocking issue${diff - blockingDiff === 1 ? '' : 's'}`)
|
|
274
|
+
} else if (diff < 0 && diff !== blockingDiff) {
|
|
275
|
+
parts.push(`${Math.abs(diff - blockingDiff)} non-blocking issue${Math.abs(diff - blockingDiff) === 1 ? '' : 's'} resolved`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
md += parts.join(' • ') + '\n\n'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return md
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Format scan metadata
|
|
286
|
+
*/
|
|
287
|
+
function formatScanMetadata(result: ScanResult, scanDepth?: string): string {
|
|
288
|
+
let md = `<details>\n<summary>📋 Scan Details</summary>\n\n`
|
|
289
|
+
md += `| Metric | Value |\n`
|
|
290
|
+
md += `|--------|-------|\n`
|
|
291
|
+
md += `| Files scanned | ${result.filesScanned} |\n`
|
|
292
|
+
md += `| Files skipped | ${result.filesSkipped} |\n`
|
|
293
|
+
md += `| Scan duration | ${(result.scanDuration / 1000).toFixed(1)}s |\n`
|
|
294
|
+
if (scanDepth) {
|
|
295
|
+
const depthLabels: Record<string, string> = {
|
|
296
|
+
cheap: 'Fast (pattern matching)',
|
|
297
|
+
validated: 'Validated (AI-assisted)',
|
|
298
|
+
deep: 'Deep (full semantic)',
|
|
299
|
+
}
|
|
300
|
+
md += `| Scan depth | ${depthLabels[scanDepth] || scanDepth} |\n`
|
|
301
|
+
}
|
|
302
|
+
md += `| Timestamp | ${result.timestamp} |\n`
|
|
303
|
+
md += `\n</details>\n\n`
|
|
304
|
+
return md
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Format quick actions section
|
|
309
|
+
*/
|
|
310
|
+
function formatQuickActions(hasBlockingIssues: boolean): string {
|
|
311
|
+
let md = `## 🎯 Quick Actions\n\n`
|
|
312
|
+
|
|
313
|
+
if (hasBlockingIssues) {
|
|
314
|
+
md += `- [ ] Fix all blocking issues before merging\n`
|
|
315
|
+
md += `- [ ] Review medium/low severity findings\n`
|
|
316
|
+
} else {
|
|
317
|
+
md += `- [ ] Review findings and address as needed\n`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
md += `- [ ] Mark false positives with \`// oculum-ignore\` comment\n`
|
|
321
|
+
md += `- 📖 [View documentation](https://oculum.dev/docs)\n`
|
|
322
|
+
md += `- 🐛 [Report false positive](https://github.com/oculum-security/oculum/issues/new?template=false-positive.md)\n\n`
|
|
323
|
+
|
|
324
|
+
return md
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Format footer
|
|
329
|
+
*/
|
|
330
|
+
function formatFooter(): string {
|
|
331
|
+
let md = `---\n\n`
|
|
332
|
+
md += `<div align="center">\n\n`
|
|
333
|
+
md += `🛡️ Powered by [Oculum Security Scanner](https://oculum.dev) • `
|
|
334
|
+
md += `[Documentation](https://oculum.dev/docs) • `
|
|
335
|
+
md += `[Get Pro](https://oculum.dev/pricing)\n\n`
|
|
336
|
+
md += `</div>\n`
|
|
337
|
+
return md
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Format scan result as a short status comment (for check run summary)
|
|
342
|
+
*/
|
|
343
|
+
export function formatShortStatus(result: ScanResult): string {
|
|
344
|
+
const { severityCounts, hasBlockingIssues, filesScanned, scanDuration } = result
|
|
345
|
+
|
|
346
|
+
if (hasBlockingIssues) {
|
|
347
|
+
const blocking = severityCounts.critical + severityCounts.high
|
|
348
|
+
return `🚨 Found ${blocking} blocking security issue${blocking === 1 ? '' : 's'} (${severityCounts.critical} critical, ${severityCounts.high} high)`
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const total = Object.values(severityCounts).reduce((a, b) => a + b, 0)
|
|
352
|
+
if (total > 0) {
|
|
353
|
+
return `⚠️ Found ${total} issue${total === 1 ? '' : 's'} (${severityCounts.medium} medium, ${severityCounts.low} low, ${severityCounts.info} info)`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return `✅ No security issues found (scanned ${filesScanned} files in ${(scanDuration / 1000).toFixed(1)}s)`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Format as inline annotation for GitHub check run
|
|
361
|
+
*/
|
|
362
|
+
export function formatAnnotation(finding: Vulnerability): {
|
|
363
|
+
path: string
|
|
364
|
+
start_line: number
|
|
365
|
+
end_line: number
|
|
366
|
+
annotation_level: 'failure' | 'warning' | 'notice'
|
|
367
|
+
message: string
|
|
368
|
+
title: string
|
|
369
|
+
} {
|
|
370
|
+
const level: 'failure' | 'warning' | 'notice' =
|
|
371
|
+
finding.severity === 'critical' || finding.severity === 'high' ? 'failure' :
|
|
372
|
+
finding.severity === 'medium' ? 'warning' : 'notice'
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
path: finding.filePath,
|
|
376
|
+
start_line: finding.lineNumber,
|
|
377
|
+
end_line: finding.lineNumber,
|
|
378
|
+
annotation_level: level,
|
|
379
|
+
title: `${SEVERITY_BADGE[finding.severity]} ${finding.title}`,
|
|
380
|
+
message: finding.description + (finding.suggestedFix ? `\n\n💡 Fix: ${finding.suggestedFix}` : ''),
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding Grouping Logic
|
|
3
|
+
* Groups and sorts vulnerabilities for workflow-friendly output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Vulnerability, VulnerabilitySeverity, VulnerabilityCategory } from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Risk themes for grouping findings
|
|
10
|
+
*/
|
|
11
|
+
export type RiskTheme =
|
|
12
|
+
| 'secrets' // Hardcoded secrets, high entropy strings
|
|
13
|
+
| 'injection' // SQL, XSS, command injection
|
|
14
|
+
| 'auth' // Missing auth, weak auth patterns
|
|
15
|
+
| 'ai' // AI-specific vulnerabilities
|
|
16
|
+
| 'config' // Insecure configuration
|
|
17
|
+
| 'data' // Data exposure, sensitive variables
|
|
18
|
+
| 'other' // Uncategorized
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Map categories to risk themes
|
|
22
|
+
*/
|
|
23
|
+
export function getRiskTheme(category: VulnerabilityCategory): RiskTheme {
|
|
24
|
+
switch (category) {
|
|
25
|
+
case 'hardcoded_secret':
|
|
26
|
+
case 'high_entropy_string':
|
|
27
|
+
case 'sensitive_url':
|
|
28
|
+
return 'secrets'
|
|
29
|
+
|
|
30
|
+
case 'sql_injection':
|
|
31
|
+
case 'xss':
|
|
32
|
+
case 'command_injection':
|
|
33
|
+
case 'dangerous_function':
|
|
34
|
+
return 'injection'
|
|
35
|
+
|
|
36
|
+
case 'missing_auth':
|
|
37
|
+
case 'security_bypass':
|
|
38
|
+
return 'auth'
|
|
39
|
+
|
|
40
|
+
case 'ai_pattern':
|
|
41
|
+
case 'ai_prompt_injection':
|
|
42
|
+
case 'ai_unsafe_execution':
|
|
43
|
+
case 'ai_overpermissive_tool':
|
|
44
|
+
return 'ai'
|
|
45
|
+
|
|
46
|
+
case 'insecure_config':
|
|
47
|
+
case 'cors_misconfiguration':
|
|
48
|
+
case 'root_container':
|
|
49
|
+
case 'dangerous_file':
|
|
50
|
+
case 'weak_crypto':
|
|
51
|
+
return 'config'
|
|
52
|
+
|
|
53
|
+
case 'data_exposure':
|
|
54
|
+
case 'sensitive_variable':
|
|
55
|
+
return 'data'
|
|
56
|
+
|
|
57
|
+
default:
|
|
58
|
+
return 'other'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Theme display names and icons
|
|
64
|
+
*/
|
|
65
|
+
export const THEME_CONFIG: Record<RiskTheme, { name: string; icon: string; priority: number }> = {
|
|
66
|
+
secrets: { name: 'Secrets & Credentials', icon: '🔑', priority: 1 },
|
|
67
|
+
injection: { name: 'Injection Vulnerabilities', icon: '💉', priority: 2 },
|
|
68
|
+
auth: { name: 'Authentication Issues', icon: '🔒', priority: 3 },
|
|
69
|
+
ai: { name: 'AI Security', icon: '🤖', priority: 4 },
|
|
70
|
+
config: { name: 'Configuration Issues', icon: '⚙️', priority: 5 },
|
|
71
|
+
data: { name: 'Data Exposure', icon: '📊', priority: 6 },
|
|
72
|
+
other: { name: 'Other Issues', icon: '⚠️', priority: 7 },
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Severity sort order (higher = more severe)
|
|
77
|
+
*/
|
|
78
|
+
const SEVERITY_ORDER: Record<VulnerabilitySeverity, number> = {
|
|
79
|
+
critical: 5,
|
|
80
|
+
high: 4,
|
|
81
|
+
medium: 3,
|
|
82
|
+
low: 2,
|
|
83
|
+
info: 1,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Grouped findings by risk theme
|
|
88
|
+
*/
|
|
89
|
+
export interface GroupedFindings {
|
|
90
|
+
theme: RiskTheme
|
|
91
|
+
themeName: string
|
|
92
|
+
themeIcon: string
|
|
93
|
+
findings: Vulnerability[]
|
|
94
|
+
severityCounts: Record<VulnerabilitySeverity, number>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Group vulnerabilities by risk theme
|
|
99
|
+
*/
|
|
100
|
+
export function groupByTheme(vulnerabilities: Vulnerability[]): GroupedFindings[] {
|
|
101
|
+
// Group by theme
|
|
102
|
+
const groups = new Map<RiskTheme, Vulnerability[]>()
|
|
103
|
+
|
|
104
|
+
for (const vuln of vulnerabilities) {
|
|
105
|
+
const theme = getRiskTheme(vuln.category)
|
|
106
|
+
if (!groups.has(theme)) {
|
|
107
|
+
groups.set(theme, [])
|
|
108
|
+
}
|
|
109
|
+
groups.get(theme)!.push(vuln)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Convert to array and sort
|
|
113
|
+
const result: GroupedFindings[] = []
|
|
114
|
+
|
|
115
|
+
for (const [theme, findings] of groups) {
|
|
116
|
+
// Sort findings within group: severity desc, then confidence desc
|
|
117
|
+
findings.sort((a, b) => {
|
|
118
|
+
const severityDiff = SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity]
|
|
119
|
+
if (severityDiff !== 0) return severityDiff
|
|
120
|
+
|
|
121
|
+
const confidenceOrder = { high: 3, medium: 2, low: 1 }
|
|
122
|
+
return confidenceOrder[b.confidence] - confidenceOrder[a.confidence]
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Count severities
|
|
126
|
+
const severityCounts: Record<VulnerabilitySeverity, number> = {
|
|
127
|
+
critical: 0,
|
|
128
|
+
high: 0,
|
|
129
|
+
medium: 0,
|
|
130
|
+
low: 0,
|
|
131
|
+
info: 0,
|
|
132
|
+
}
|
|
133
|
+
for (const f of findings) {
|
|
134
|
+
severityCounts[f.severity]++
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const config = THEME_CONFIG[theme]
|
|
138
|
+
result.push({
|
|
139
|
+
theme,
|
|
140
|
+
themeName: config.name,
|
|
141
|
+
themeIcon: config.icon,
|
|
142
|
+
findings,
|
|
143
|
+
severityCounts,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Sort groups by theme priority
|
|
148
|
+
result.sort((a, b) => THEME_CONFIG[a.theme].priority - THEME_CONFIG[b.theme].priority)
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Limit findings per group to avoid overwhelming output
|
|
155
|
+
*/
|
|
156
|
+
export function limitPerGroup(groups: GroupedFindings[], maxPerGroup: number = 10): GroupedFindings[] {
|
|
157
|
+
return groups.map(group => ({
|
|
158
|
+
...group,
|
|
159
|
+
findings: group.findings.slice(0, maxPerGroup),
|
|
160
|
+
}))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get findings sorted by severity across all groups
|
|
165
|
+
*/
|
|
166
|
+
export function sortBySeverity(vulnerabilities: Vulnerability[]): Vulnerability[] {
|
|
167
|
+
return [...vulnerabilities].sort((a, b) => {
|
|
168
|
+
const severityDiff = SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity]
|
|
169
|
+
if (severityDiff !== 0) return severityDiff
|
|
170
|
+
|
|
171
|
+
const confidenceOrder = { high: 3, medium: 2, low: 1 }
|
|
172
|
+
return confidenceOrder[b.confidence] - confidenceOrder[a.confidence]
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Filter to only blocking issues (critical/high)
|
|
178
|
+
*/
|
|
179
|
+
export function getBlockingIssues(vulnerabilities: Vulnerability[]): Vulnerability[] {
|
|
180
|
+
return vulnerabilities.filter(v => v.severity === 'critical' || v.severity === 'high')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Filter to actionable issues (medium and above)
|
|
185
|
+
*/
|
|
186
|
+
export function getActionableIssues(vulnerabilities: Vulnerability[]): Vulnerability[] {
|
|
187
|
+
return vulnerabilities.filter(v =>
|
|
188
|
+
v.severity === 'critical' || v.severity === 'high' || v.severity === 'medium'
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Formatters
|
|
3
|
+
* Export all formatting utilities for different workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Grouping utilities
|
|
7
|
+
export {
|
|
8
|
+
groupByTheme,
|
|
9
|
+
limitPerGroup,
|
|
10
|
+
sortBySeverity,
|
|
11
|
+
getBlockingIssues,
|
|
12
|
+
getActionableIssues,
|
|
13
|
+
getRiskTheme,
|
|
14
|
+
THEME_CONFIG,
|
|
15
|
+
type RiskTheme,
|
|
16
|
+
type GroupedFindings,
|
|
17
|
+
} from './grouping'
|
|
18
|
+
|
|
19
|
+
// GitHub comment formatter
|
|
20
|
+
export {
|
|
21
|
+
formatGitHubComment,
|
|
22
|
+
formatShortStatus,
|
|
23
|
+
formatAnnotation,
|
|
24
|
+
type GitHubCommentOptions,
|
|
25
|
+
} from './github-comment'
|
|
26
|
+
|
|
27
|
+
// VS Code diagnostic formatter
|
|
28
|
+
export {
|
|
29
|
+
formatDiagnostic,
|
|
30
|
+
formatDiagnosticsByFile,
|
|
31
|
+
generateCodeAction,
|
|
32
|
+
formatForProblemsPanel,
|
|
33
|
+
DiagnosticSeverity,
|
|
34
|
+
type Diagnostic,
|
|
35
|
+
type DiagnosticsByFile,
|
|
36
|
+
type CodeAction,
|
|
37
|
+
type Position,
|
|
38
|
+
type Range,
|
|
39
|
+
} from './vscode-diagnostic'
|
|
40
|
+
|
|
41
|
+
// CLI terminal formatter
|
|
42
|
+
export {
|
|
43
|
+
formatTerminalOutput,
|
|
44
|
+
formatSimpleList,
|
|
45
|
+
formatJSON,
|
|
46
|
+
formatSARIF,
|
|
47
|
+
} from './cli-terminal'
|