@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.
Files changed (281) hide show
  1. package/dist/formatters/cli-terminal.d.ts +27 -0
  2. package/dist/formatters/cli-terminal.d.ts.map +1 -0
  3. package/dist/formatters/cli-terminal.js +412 -0
  4. package/dist/formatters/cli-terminal.js.map +1 -0
  5. package/dist/formatters/github-comment.d.ts +41 -0
  6. package/dist/formatters/github-comment.d.ts.map +1 -0
  7. package/dist/formatters/github-comment.js +306 -0
  8. package/dist/formatters/github-comment.js.map +1 -0
  9. package/dist/formatters/grouping.d.ts +52 -0
  10. package/dist/formatters/grouping.d.ts.map +1 -0
  11. package/dist/formatters/grouping.js +152 -0
  12. package/dist/formatters/grouping.js.map +1 -0
  13. package/dist/formatters/index.d.ts +9 -0
  14. package/dist/formatters/index.d.ts.map +1 -0
  15. package/dist/formatters/index.js +35 -0
  16. package/dist/formatters/index.js.map +1 -0
  17. package/dist/formatters/vscode-diagnostic.d.ts +103 -0
  18. package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
  19. package/dist/formatters/vscode-diagnostic.js +151 -0
  20. package/dist/formatters/vscode-diagnostic.js.map +1 -0
  21. package/dist/index.d.ts +52 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +648 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layer1/comments.d.ts +8 -0
  26. package/dist/layer1/comments.d.ts.map +1 -0
  27. package/dist/layer1/comments.js +203 -0
  28. package/dist/layer1/comments.js.map +1 -0
  29. package/dist/layer1/config-audit.d.ts +8 -0
  30. package/dist/layer1/config-audit.d.ts.map +1 -0
  31. package/dist/layer1/config-audit.js +252 -0
  32. package/dist/layer1/config-audit.js.map +1 -0
  33. package/dist/layer1/entropy.d.ts +8 -0
  34. package/dist/layer1/entropy.d.ts.map +1 -0
  35. package/dist/layer1/entropy.js +500 -0
  36. package/dist/layer1/entropy.js.map +1 -0
  37. package/dist/layer1/file-flags.d.ts +7 -0
  38. package/dist/layer1/file-flags.d.ts.map +1 -0
  39. package/dist/layer1/file-flags.js +112 -0
  40. package/dist/layer1/file-flags.js.map +1 -0
  41. package/dist/layer1/index.d.ts +36 -0
  42. package/dist/layer1/index.d.ts.map +1 -0
  43. package/dist/layer1/index.js +132 -0
  44. package/dist/layer1/index.js.map +1 -0
  45. package/dist/layer1/patterns.d.ts +8 -0
  46. package/dist/layer1/patterns.d.ts.map +1 -0
  47. package/dist/layer1/patterns.js +482 -0
  48. package/dist/layer1/patterns.js.map +1 -0
  49. package/dist/layer1/urls.d.ts +8 -0
  50. package/dist/layer1/urls.d.ts.map +1 -0
  51. package/dist/layer1/urls.js +296 -0
  52. package/dist/layer1/urls.js.map +1 -0
  53. package/dist/layer1/weak-crypto.d.ts +7 -0
  54. package/dist/layer1/weak-crypto.d.ts.map +1 -0
  55. package/dist/layer1/weak-crypto.js +291 -0
  56. package/dist/layer1/weak-crypto.js.map +1 -0
  57. package/dist/layer2/ai-agent-tools.d.ts +19 -0
  58. package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
  59. package/dist/layer2/ai-agent-tools.js +528 -0
  60. package/dist/layer2/ai-agent-tools.js.map +1 -0
  61. package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
  62. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
  63. package/dist/layer2/ai-endpoint-protection.js +332 -0
  64. package/dist/layer2/ai-endpoint-protection.js.map +1 -0
  65. package/dist/layer2/ai-execution-sinks.d.ts +18 -0
  66. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
  67. package/dist/layer2/ai-execution-sinks.js +496 -0
  68. package/dist/layer2/ai-execution-sinks.js.map +1 -0
  69. package/dist/layer2/ai-fingerprinting.d.ts +7 -0
  70. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
  71. package/dist/layer2/ai-fingerprinting.js +654 -0
  72. package/dist/layer2/ai-fingerprinting.js.map +1 -0
  73. package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
  74. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
  75. package/dist/layer2/ai-prompt-hygiene.js +356 -0
  76. package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
  77. package/dist/layer2/ai-rag-safety.d.ts +21 -0
  78. package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
  79. package/dist/layer2/ai-rag-safety.js +459 -0
  80. package/dist/layer2/ai-rag-safety.js.map +1 -0
  81. package/dist/layer2/ai-schema-validation.d.ts +25 -0
  82. package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
  83. package/dist/layer2/ai-schema-validation.js +375 -0
  84. package/dist/layer2/ai-schema-validation.js.map +1 -0
  85. package/dist/layer2/auth-antipatterns.d.ts +20 -0
  86. package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
  87. package/dist/layer2/auth-antipatterns.js +333 -0
  88. package/dist/layer2/auth-antipatterns.js.map +1 -0
  89. package/dist/layer2/byok-patterns.d.ts +12 -0
  90. package/dist/layer2/byok-patterns.d.ts.map +1 -0
  91. package/dist/layer2/byok-patterns.js +299 -0
  92. package/dist/layer2/byok-patterns.js.map +1 -0
  93. package/dist/layer2/dangerous-functions.d.ts +7 -0
  94. package/dist/layer2/dangerous-functions.d.ts.map +1 -0
  95. package/dist/layer2/dangerous-functions.js +1375 -0
  96. package/dist/layer2/dangerous-functions.js.map +1 -0
  97. package/dist/layer2/data-exposure.d.ts +16 -0
  98. package/dist/layer2/data-exposure.d.ts.map +1 -0
  99. package/dist/layer2/data-exposure.js +279 -0
  100. package/dist/layer2/data-exposure.js.map +1 -0
  101. package/dist/layer2/framework-checks.d.ts +7 -0
  102. package/dist/layer2/framework-checks.d.ts.map +1 -0
  103. package/dist/layer2/framework-checks.js +388 -0
  104. package/dist/layer2/framework-checks.js.map +1 -0
  105. package/dist/layer2/index.d.ts +58 -0
  106. package/dist/layer2/index.d.ts.map +1 -0
  107. package/dist/layer2/index.js +380 -0
  108. package/dist/layer2/index.js.map +1 -0
  109. package/dist/layer2/logic-gates.d.ts +7 -0
  110. package/dist/layer2/logic-gates.d.ts.map +1 -0
  111. package/dist/layer2/logic-gates.js +182 -0
  112. package/dist/layer2/logic-gates.js.map +1 -0
  113. package/dist/layer2/risky-imports.d.ts +7 -0
  114. package/dist/layer2/risky-imports.d.ts.map +1 -0
  115. package/dist/layer2/risky-imports.js +161 -0
  116. package/dist/layer2/risky-imports.js.map +1 -0
  117. package/dist/layer2/variables.d.ts +8 -0
  118. package/dist/layer2/variables.d.ts.map +1 -0
  119. package/dist/layer2/variables.js +152 -0
  120. package/dist/layer2/variables.js.map +1 -0
  121. package/dist/layer3/anthropic.d.ts +83 -0
  122. package/dist/layer3/anthropic.d.ts.map +1 -0
  123. package/dist/layer3/anthropic.js +1745 -0
  124. package/dist/layer3/anthropic.js.map +1 -0
  125. package/dist/layer3/index.d.ts +24 -0
  126. package/dist/layer3/index.d.ts.map +1 -0
  127. package/dist/layer3/index.js +119 -0
  128. package/dist/layer3/index.js.map +1 -0
  129. package/dist/layer3/openai.d.ts +25 -0
  130. package/dist/layer3/openai.d.ts.map +1 -0
  131. package/dist/layer3/openai.js +238 -0
  132. package/dist/layer3/openai.js.map +1 -0
  133. package/dist/layer3/package-check.d.ts +63 -0
  134. package/dist/layer3/package-check.d.ts.map +1 -0
  135. package/dist/layer3/package-check.js +508 -0
  136. package/dist/layer3/package-check.js.map +1 -0
  137. package/dist/modes/incremental.d.ts +66 -0
  138. package/dist/modes/incremental.d.ts.map +1 -0
  139. package/dist/modes/incremental.js +200 -0
  140. package/dist/modes/incremental.js.map +1 -0
  141. package/dist/tiers.d.ts +125 -0
  142. package/dist/tiers.d.ts.map +1 -0
  143. package/dist/tiers.js +234 -0
  144. package/dist/tiers.js.map +1 -0
  145. package/dist/types.d.ts +175 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +50 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/utils/auth-helper-detector.d.ts +56 -0
  150. package/dist/utils/auth-helper-detector.d.ts.map +1 -0
  151. package/dist/utils/auth-helper-detector.js +360 -0
  152. package/dist/utils/auth-helper-detector.js.map +1 -0
  153. package/dist/utils/context-helpers.d.ts +96 -0
  154. package/dist/utils/context-helpers.d.ts.map +1 -0
  155. package/dist/utils/context-helpers.js +493 -0
  156. package/dist/utils/context-helpers.js.map +1 -0
  157. package/dist/utils/diff-detector.d.ts +53 -0
  158. package/dist/utils/diff-detector.d.ts.map +1 -0
  159. package/dist/utils/diff-detector.js +104 -0
  160. package/dist/utils/diff-detector.js.map +1 -0
  161. package/dist/utils/diff-parser.d.ts +80 -0
  162. package/dist/utils/diff-parser.d.ts.map +1 -0
  163. package/dist/utils/diff-parser.js +202 -0
  164. package/dist/utils/diff-parser.js.map +1 -0
  165. package/dist/utils/imported-auth-detector.d.ts +37 -0
  166. package/dist/utils/imported-auth-detector.d.ts.map +1 -0
  167. package/dist/utils/imported-auth-detector.js +251 -0
  168. package/dist/utils/imported-auth-detector.js.map +1 -0
  169. package/dist/utils/middleware-detector.d.ts +55 -0
  170. package/dist/utils/middleware-detector.d.ts.map +1 -0
  171. package/dist/utils/middleware-detector.js +260 -0
  172. package/dist/utils/middleware-detector.js.map +1 -0
  173. package/dist/utils/oauth-flow-detector.d.ts +41 -0
  174. package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
  175. package/dist/utils/oauth-flow-detector.js +202 -0
  176. package/dist/utils/oauth-flow-detector.js.map +1 -0
  177. package/dist/utils/path-exclusions.d.ts +55 -0
  178. package/dist/utils/path-exclusions.d.ts.map +1 -0
  179. package/dist/utils/path-exclusions.js +222 -0
  180. package/dist/utils/path-exclusions.js.map +1 -0
  181. package/dist/utils/project-context-builder.d.ts +119 -0
  182. package/dist/utils/project-context-builder.d.ts.map +1 -0
  183. package/dist/utils/project-context-builder.js +534 -0
  184. package/dist/utils/project-context-builder.js.map +1 -0
  185. package/dist/utils/registry-clients.d.ts +93 -0
  186. package/dist/utils/registry-clients.d.ts.map +1 -0
  187. package/dist/utils/registry-clients.js +273 -0
  188. package/dist/utils/registry-clients.js.map +1 -0
  189. package/dist/utils/trpc-analyzer.d.ts +78 -0
  190. package/dist/utils/trpc-analyzer.d.ts.map +1 -0
  191. package/dist/utils/trpc-analyzer.js +297 -0
  192. package/dist/utils/trpc-analyzer.js.map +1 -0
  193. package/package.json +45 -0
  194. package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
  195. package/src/__tests__/benchmark/fixtures/index.ts +68 -0
  196. package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
  197. package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
  198. package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
  199. package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
  200. package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
  201. package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
  202. package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
  203. package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
  204. package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
  205. package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
  206. package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
  207. package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
  208. package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
  209. package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
  210. package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
  211. package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
  212. package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
  213. package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
  214. package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
  215. package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
  216. package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
  217. package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
  218. package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
  219. package/src/__tests__/benchmark/index.ts +29 -0
  220. package/src/__tests__/benchmark/run-benchmark.ts +144 -0
  221. package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
  222. package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
  223. package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
  224. package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
  225. package/src/__tests__/benchmark/types.ts +144 -0
  226. package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
  227. package/src/__tests__/regression/known-false-positives.test.ts +467 -0
  228. package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
  229. package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
  230. package/src/__tests__/validation/analyze-results.ts +542 -0
  231. package/src/__tests__/validation/extract-for-triage.ts +146 -0
  232. package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
  233. package/src/__tests__/validation/run-validation.ts +364 -0
  234. package/src/__tests__/validation/triage-template.md +132 -0
  235. package/src/formatters/cli-terminal.ts +446 -0
  236. package/src/formatters/github-comment.ts +382 -0
  237. package/src/formatters/grouping.ts +190 -0
  238. package/src/formatters/index.ts +47 -0
  239. package/src/formatters/vscode-diagnostic.ts +243 -0
  240. package/src/index.ts +823 -0
  241. package/src/layer1/comments.ts +218 -0
  242. package/src/layer1/config-audit.ts +289 -0
  243. package/src/layer1/entropy.ts +583 -0
  244. package/src/layer1/file-flags.ts +127 -0
  245. package/src/layer1/index.ts +181 -0
  246. package/src/layer1/patterns.ts +516 -0
  247. package/src/layer1/urls.ts +334 -0
  248. package/src/layer1/weak-crypto.ts +328 -0
  249. package/src/layer2/ai-agent-tools.ts +601 -0
  250. package/src/layer2/ai-endpoint-protection.ts +387 -0
  251. package/src/layer2/ai-execution-sinks.ts +580 -0
  252. package/src/layer2/ai-fingerprinting.ts +758 -0
  253. package/src/layer2/ai-prompt-hygiene.ts +411 -0
  254. package/src/layer2/ai-rag-safety.ts +511 -0
  255. package/src/layer2/ai-schema-validation.ts +421 -0
  256. package/src/layer2/auth-antipatterns.ts +394 -0
  257. package/src/layer2/byok-patterns.ts +336 -0
  258. package/src/layer2/dangerous-functions.ts +1563 -0
  259. package/src/layer2/data-exposure.ts +315 -0
  260. package/src/layer2/framework-checks.ts +433 -0
  261. package/src/layer2/index.ts +473 -0
  262. package/src/layer2/logic-gates.ts +206 -0
  263. package/src/layer2/risky-imports.ts +186 -0
  264. package/src/layer2/variables.ts +166 -0
  265. package/src/layer3/anthropic.ts +2030 -0
  266. package/src/layer3/index.ts +130 -0
  267. package/src/layer3/package-check.ts +604 -0
  268. package/src/modes/incremental.ts +293 -0
  269. package/src/tiers.ts +318 -0
  270. package/src/types.ts +284 -0
  271. package/src/utils/auth-helper-detector.ts +443 -0
  272. package/src/utils/context-helpers.ts +535 -0
  273. package/src/utils/diff-detector.ts +135 -0
  274. package/src/utils/diff-parser.ts +272 -0
  275. package/src/utils/imported-auth-detector.ts +320 -0
  276. package/src/utils/middleware-detector.ts +333 -0
  277. package/src/utils/oauth-flow-detector.ts +246 -0
  278. package/src/utils/path-exclusions.ts +266 -0
  279. package/src/utils/project-context-builder.ts +707 -0
  280. package/src/utils/registry-clients.ts +351 -0
  281. 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'