@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,132 @@
1
+ # M7: Finding Triage Template
2
+
3
+ Use this template to document triage decisions for each medium+ severity finding from real-repo validation.
4
+
5
+ ## Instructions
6
+
7
+ 1. For each medium+ finding from the validation scan:
8
+ - Copy the template below
9
+ - Fill in the finding details
10
+ - Classify as TP/FP/Borderline
11
+ - Document your reasoning
12
+ - Decide on action
13
+
14
+ 2. After triage, calculate FP rate:
15
+ - FP Rate = FP count / (TP count + FP count)
16
+ - Target: < 20%
17
+
18
+ 3. For each FP:
19
+ - Add a regression test to `known-false-positives.test.ts`
20
+ - Consider tuning the detector if pattern is common
21
+
22
+ ---
23
+
24
+ ## Triage Records
25
+
26
+ ### [Finding #1]
27
+
28
+ **Category:** [e.g., ai_rag_exfiltration]
29
+ **File:** [e.g., langchainjs/src/retrievers/base.ts]
30
+ **Line:** [e.g., 142]
31
+ **Severity:** [critical/high/medium]
32
+ **Title:** [Finding title from scan]
33
+
34
+ **Code Context:**
35
+ ```typescript
36
+ // Paste relevant code snippet here
37
+ ```
38
+
39
+ **Classification:** [ ] True Positive [ ] False Positive [ ] Borderline
40
+
41
+ **Code Type:** [ ] Library internal [ ] Example code [ ] Test file [ ] Other
42
+
43
+ **Reasoning:**
44
+ > [Explain why this is/isn't a real security issue. Consider:
45
+ > - Is this intentional design (library deferring to consumers)?
46
+ > - Is this example/demo code?
47
+ > - Is the finding contextually accurate?]
48
+
49
+ **Action:**
50
+ - [ ] Keep finding as-is
51
+ - [ ] Tune detector (describe change needed)
52
+ - [ ] Add to known-FP fixtures
53
+ - [ ] Downgrade severity
54
+
55
+ **Notes:**
56
+ > [Any additional context or follow-up needed]
57
+
58
+ ---
59
+
60
+ ### [Finding #2]
61
+
62
+ **Category:**
63
+ **File:**
64
+ **Line:**
65
+ **Severity:**
66
+ **Title:**
67
+
68
+ **Code Context:**
69
+ ```typescript
70
+ ```
71
+
72
+ **Classification:** [ ] True Positive [ ] False Positive [ ] Borderline
73
+
74
+ **Code Type:** [ ] Library internal [ ] Example code [ ] Test file [ ] Other
75
+
76
+ **Reasoning:**
77
+ >
78
+
79
+ **Action:**
80
+ - [ ] Keep finding as-is
81
+ - [ ] Tune detector
82
+ - [ ] Add to known-FP fixtures
83
+ - [ ] Downgrade severity
84
+
85
+ **Notes:**
86
+ >
87
+
88
+ ---
89
+
90
+ ## Summary Statistics
91
+
92
+ After completing triage:
93
+
94
+ | Metric | Count |
95
+ |--------|-------|
96
+ | Total medium+ findings | |
97
+ | True Positives | |
98
+ | False Positives | |
99
+ | Borderline | |
100
+ | **FP Rate** | % |
101
+
102
+ ### Notable True Positives
103
+
104
+ List significant security issues found:
105
+
106
+ 1.
107
+ 2.
108
+ 3.
109
+
110
+ ### Common False Positive Patterns
111
+
112
+ List patterns that frequently triggered FPs:
113
+
114
+ 1.
115
+ 2.
116
+ 3.
117
+
118
+ ### Recommended Detector Tuning
119
+
120
+ Based on FP analysis:
121
+
122
+ 1. **[Detector name]**: [Suggested change]
123
+ 2. **[Detector name]**: [Suggested change]
124
+
125
+ ---
126
+
127
+ ## Action Items
128
+
129
+ - [ ] Add FP regression tests
130
+ - [ ] Update detectors based on findings
131
+ - [ ] Re-run validation to verify improvements
132
+ - [ ] Update docs/RESULTSCOMPARISON.md with final metrics
@@ -0,0 +1,446 @@
1
+ /**
2
+ * CLI Terminal Formatter
3
+ * Formats scan results with ANSI colors for terminal output
4
+ */
5
+
6
+ import type { ScanResult, Vulnerability, VulnerabilitySeverity } from '../types'
7
+ import { groupByTheme, getBlockingIssues, GroupedFindings, THEME_CONFIG } from './grouping'
8
+
9
+ /**
10
+ * ANSI color codes
11
+ */
12
+ const colors = {
13
+ reset: '\x1b[0m',
14
+ bold: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ underline: '\x1b[4m',
17
+
18
+ // Foreground colors
19
+ red: '\x1b[31m',
20
+ green: '\x1b[32m',
21
+ yellow: '\x1b[33m',
22
+ blue: '\x1b[34m',
23
+ magenta: '\x1b[35m',
24
+ cyan: '\x1b[36m',
25
+ white: '\x1b[37m',
26
+ gray: '\x1b[90m',
27
+
28
+ // Background colors
29
+ bgRed: '\x1b[41m',
30
+ bgYellow: '\x1b[43m',
31
+ bgBlue: '\x1b[44m',
32
+ }
33
+
34
+ /**
35
+ * Severity colors and symbols
36
+ */
37
+ const SEVERITY_STYLE: Record<VulnerabilitySeverity, { color: string; symbol: string; label: string }> = {
38
+ critical: { color: colors.bgRed + colors.white, symbol: '●', label: 'CRITICAL' },
39
+ high: { color: colors.red, symbol: '●', label: 'HIGH' },
40
+ medium: { color: colors.yellow, symbol: '●', label: 'MEDIUM' },
41
+ low: { color: colors.blue, symbol: '○', label: 'LOW' },
42
+ info: { color: colors.gray, symbol: '○', label: 'INFO' },
43
+ }
44
+
45
+ /**
46
+ * Format colored text
47
+ */
48
+ function c(color: string, text: string): string {
49
+ return `${color}${text}${colors.reset}`
50
+ }
51
+
52
+ /**
53
+ * Format severity badge
54
+ */
55
+ function severityBadge(severity: VulnerabilitySeverity): string {
56
+ const style = SEVERITY_STYLE[severity]
57
+ return c(style.color, `${style.symbol} ${style.label}`)
58
+ }
59
+
60
+ /**
61
+ * Format a single finding for terminal
62
+ */
63
+ function formatFinding(finding: Vulnerability, indent: string = ' '): string {
64
+ const badge = severityBadge(finding.severity)
65
+ const location = c(colors.cyan, `${finding.filePath}:${finding.lineNumber}`)
66
+
67
+ let output = `${indent}${badge} ${c(colors.bold, finding.title)}\n`
68
+ output += `${indent} ${location}\n`
69
+ output += `${indent} ${c(colors.dim, finding.description)}\n`
70
+
71
+ if (finding.suggestedFix) {
72
+ output += `${indent} ${c(colors.green, '💡 ' + finding.suggestedFix)}\n`
73
+ }
74
+
75
+ return output
76
+ }
77
+
78
+ /**
79
+ * Format a group of findings
80
+ */
81
+ function formatGroup(group: GroupedFindings, maxFindings: number = 10): string {
82
+ const { theme, themeName, findings, severityCounts } = group
83
+ const config = THEME_CONFIG[theme]
84
+
85
+ // Count summary
86
+ const counts: string[] = []
87
+ if (severityCounts.critical > 0) counts.push(c(colors.red, `${severityCounts.critical} critical`))
88
+ if (severityCounts.high > 0) counts.push(c(colors.red, `${severityCounts.high} high`))
89
+ if (severityCounts.medium > 0) counts.push(c(colors.yellow, `${severityCounts.medium} medium`))
90
+ if (severityCounts.low > 0) counts.push(c(colors.blue, `${severityCounts.low} low`))
91
+ if (severityCounts.info > 0) counts.push(c(colors.gray, `${severityCounts.info} info`))
92
+
93
+ let output = `\n${c(colors.bold, `${config.icon} ${themeName}`)} (${counts.join(', ')})\n`
94
+ output += c(colors.dim, '─'.repeat(60)) + '\n'
95
+
96
+ // Show findings
97
+ const shown = findings.slice(0, maxFindings)
98
+ for (const finding of shown) {
99
+ output += formatFinding(finding) + '\n'
100
+ }
101
+
102
+ // Truncation notice
103
+ if (findings.length > maxFindings) {
104
+ output += c(colors.dim, ` ... and ${findings.length - maxFindings} more\n`)
105
+ }
106
+
107
+ return output
108
+ }
109
+
110
+ /**
111
+ * Format full scan result for terminal
112
+ */
113
+ export function formatTerminalOutput(result: ScanResult, options: {
114
+ maxFindingsPerGroup?: number
115
+ showAllFindings?: boolean
116
+ noColor?: boolean
117
+ } = {}): string {
118
+ const {
119
+ maxFindingsPerGroup = 10,
120
+ showAllFindings = false,
121
+ } = options
122
+
123
+ const { vulnerabilities, severityCounts, hasBlockingIssues, filesScanned, scanDuration } = result
124
+
125
+ let output = '\n'
126
+
127
+ // Header
128
+ output += c(colors.bold, '═'.repeat(60)) + '\n'
129
+ output += c(colors.bold, ' OCULUM SECURITY SCAN RESULTS') + '\n'
130
+ output += c(colors.bold, '═'.repeat(60)) + '\n\n'
131
+
132
+ // Status
133
+ if (hasBlockingIssues) {
134
+ const blocking = severityCounts.critical + severityCounts.high
135
+ output += c(colors.bgRed + colors.white + colors.bold, ` 🚨 ${blocking} BLOCKING ISSUES FOUND `) + '\n\n'
136
+ } else if (vulnerabilities.length > 0) {
137
+ output += c(colors.yellow, `⚠️ ${vulnerabilities.length} issues found (no blocking issues)`) + '\n\n'
138
+ } else {
139
+ output += c(colors.green, '✅ No security issues found!') + '\n\n'
140
+ output += c(colors.dim, `Scanned ${filesScanned} files in ${(scanDuration / 1000).toFixed(1)}s`) + '\n'
141
+ return output
142
+ }
143
+
144
+ // Summary counts
145
+ output += c(colors.bold, 'Summary:') + '\n'
146
+ if (severityCounts.critical > 0) output += ` ${severityBadge('critical')} ${severityCounts.critical}\n`
147
+ if (severityCounts.high > 0) output += ` ${severityBadge('high')} ${severityCounts.high}\n`
148
+ if (severityCounts.medium > 0) output += ` ${severityBadge('medium')} ${severityCounts.medium}\n`
149
+ if (severityCounts.low > 0) output += ` ${severityBadge('low')} ${severityCounts.low}\n`
150
+ if (severityCounts.info > 0) output += ` ${severityBadge('info')} ${severityCounts.info}\n`
151
+ output += '\n'
152
+
153
+ // Blocking issues first
154
+ const blockingIssues = getBlockingIssues(vulnerabilities)
155
+ if (blockingIssues.length > 0) {
156
+ output += c(colors.bgRed + colors.white + colors.bold, ' BLOCKING ISSUES ') + '\n'
157
+ output += c(colors.red, 'These must be fixed before merging:') + '\n\n'
158
+
159
+ for (const finding of blockingIssues.slice(0, 10)) {
160
+ output += formatFinding(finding)
161
+ output += '\n'
162
+ }
163
+
164
+ if (blockingIssues.length > 10) {
165
+ output += c(colors.dim, ` ... and ${blockingIssues.length - 10} more blocking issues\n`)
166
+ }
167
+
168
+ output += '\n'
169
+ }
170
+
171
+ // Grouped findings
172
+ const grouped = groupByTheme(vulnerabilities)
173
+ output += c(colors.bold, '─'.repeat(60)) + '\n'
174
+ output += c(colors.bold, 'ALL FINDINGS BY CATEGORY') + '\n'
175
+
176
+ for (const group of grouped) {
177
+ // Skip if only showing non-blocking and all are blocking
178
+ if (!showAllFindings) {
179
+ const nonBlocking = group.findings.filter(
180
+ f => f.severity !== 'critical' && f.severity !== 'high'
181
+ )
182
+ if (nonBlocking.length === 0 && blockingIssues.length > 0) continue
183
+ }
184
+
185
+ output += formatGroup(group, maxFindingsPerGroup)
186
+ }
187
+
188
+ // Footer
189
+ output += '\n' + c(colors.dim, '─'.repeat(60)) + '\n'
190
+ output += c(colors.dim, `Scanned ${filesScanned} files in ${(scanDuration / 1000).toFixed(1)}s`) + '\n'
191
+
192
+ return output
193
+ }
194
+
195
+ /**
196
+ * Format as simple list (no grouping, no colors)
197
+ */
198
+ export function formatSimpleList(vulnerabilities: Vulnerability[]): string {
199
+ let output = ''
200
+
201
+ for (const finding of vulnerabilities) {
202
+ const severity = finding.severity.toUpperCase().padEnd(8)
203
+ output += `[${severity}] ${finding.filePath}:${finding.lineNumber} - ${finding.title}\n`
204
+ }
205
+
206
+ return output
207
+ }
208
+
209
+ /**
210
+ * Format as JSON (for piping to other tools)
211
+ */
212
+ export function formatJSON(result: ScanResult, pretty: boolean = false): string {
213
+ if (pretty) {
214
+ return JSON.stringify(result, null, 2)
215
+ }
216
+ return JSON.stringify(result)
217
+ }
218
+
219
+ /**
220
+ * Rule metadata for SARIF output
221
+ */
222
+ const RULE_METADATA: Record<string, { name: string; description: string; helpUri: string; tags: string[] }> = {
223
+ hardcoded_secret: {
224
+ name: 'Hardcoded Secret',
225
+ description: 'Sensitive credentials or API keys hardcoded in source code. These can be extracted from version control history or compiled binaries.',
226
+ helpUri: 'https://oculum.dev/docs/rules/hardcoded-secrets',
227
+ tags: ['security', 'secrets', 'credentials'],
228
+ },
229
+ high_entropy_string: {
230
+ name: 'High Entropy String',
231
+ description: 'A high-entropy string that may be a secret or API key. Review to ensure it is not sensitive data.',
232
+ helpUri: 'https://oculum.dev/docs/rules/high-entropy',
233
+ tags: ['security', 'secrets'],
234
+ },
235
+ ai_prompt_injection: {
236
+ name: 'AI Prompt Injection',
237
+ description: 'User input is included in AI prompts without proper sanitization, potentially allowing prompt injection attacks.',
238
+ helpUri: 'https://oculum.dev/docs/rules/prompt-injection',
239
+ tags: ['security', 'ai', 'injection'],
240
+ },
241
+ ai_unsafe_execution: {
242
+ name: 'AI Unsafe Execution',
243
+ description: 'AI-generated content is used in code execution, SQL queries, or other dangerous sinks without validation.',
244
+ helpUri: 'https://oculum.dev/docs/rules/unsafe-execution',
245
+ tags: ['security', 'ai', 'injection'],
246
+ },
247
+ ai_overpermissive_tool: {
248
+ name: 'AI Overpermissive Tool',
249
+ description: 'AI agent tool has excessive permissions without proper restrictions or sandboxing.',
250
+ helpUri: 'https://oculum.dev/docs/rules/overpermissive-tools',
251
+ tags: ['security', 'ai', 'authorization'],
252
+ },
253
+ ai_rag_exfiltration: {
254
+ name: 'AI RAG Data Exfiltration',
255
+ description: 'RAG (Retrieval Augmented Generation) queries may expose data across tenant boundaries or leak sensitive context.',
256
+ helpUri: 'https://oculum.dev/docs/rules/rag-exfiltration',
257
+ tags: ['security', 'ai', 'data-exposure'],
258
+ },
259
+ ai_endpoint_unprotected: {
260
+ name: 'AI Endpoint Unprotected',
261
+ description: 'AI endpoint lacks authentication or rate limiting, potentially allowing abuse or cost attacks.',
262
+ helpUri: 'https://oculum.dev/docs/rules/unprotected-endpoints',
263
+ tags: ['security', 'ai', 'authentication'],
264
+ },
265
+ ai_schema_mismatch: {
266
+ name: 'AI Schema Validation Missing',
267
+ description: 'AI-generated output is used without schema validation, potentially allowing malformed or malicious data.',
268
+ helpUri: 'https://oculum.dev/docs/rules/schema-validation',
269
+ tags: ['security', 'ai', 'validation'],
270
+ },
271
+ sql_injection: {
272
+ name: 'SQL Injection',
273
+ description: 'User input is concatenated into SQL queries without parameterization, allowing SQL injection attacks.',
274
+ helpUri: 'https://oculum.dev/docs/rules/sql-injection',
275
+ tags: ['security', 'injection', 'database'],
276
+ },
277
+ xss: {
278
+ name: 'Cross-Site Scripting (XSS)',
279
+ description: 'User input is rendered in HTML without proper escaping, allowing script injection.',
280
+ helpUri: 'https://oculum.dev/docs/rules/xss',
281
+ tags: ['security', 'injection', 'web'],
282
+ },
283
+ command_injection: {
284
+ name: 'Command Injection',
285
+ description: 'User input is passed to shell commands without sanitization, allowing arbitrary command execution.',
286
+ helpUri: 'https://oculum.dev/docs/rules/command-injection',
287
+ tags: ['security', 'injection', 'shell'],
288
+ },
289
+ missing_auth: {
290
+ name: 'Missing Authentication',
291
+ description: 'Sensitive endpoint or route lacks authentication checks.',
292
+ helpUri: 'https://oculum.dev/docs/rules/missing-auth',
293
+ tags: ['security', 'authentication'],
294
+ },
295
+ data_exposure: {
296
+ name: 'Data Exposure',
297
+ description: 'Sensitive data may be exposed through logging, error messages, or API responses.',
298
+ helpUri: 'https://oculum.dev/docs/rules/data-exposure',
299
+ tags: ['security', 'data-exposure'],
300
+ },
301
+ insecure_config: {
302
+ name: 'Insecure Configuration',
303
+ description: 'Security-relevant configuration is set to an insecure value.',
304
+ helpUri: 'https://oculum.dev/docs/rules/insecure-config',
305
+ tags: ['security', 'configuration'],
306
+ },
307
+ dangerous_function: {
308
+ name: 'Dangerous Function',
309
+ description: 'Use of a function known to be dangerous or deprecated for security reasons.',
310
+ helpUri: 'https://oculum.dev/docs/rules/dangerous-functions',
311
+ tags: ['security', 'code-quality'],
312
+ },
313
+ }
314
+
315
+ /**
316
+ * Format as SARIF (Static Analysis Results Interchange Format)
317
+ * For integration with GitHub Code Scanning
318
+ */
319
+ export function formatSARIF(result: ScanResult): object {
320
+ return {
321
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
322
+ version: '2.1.0',
323
+ runs: [{
324
+ tool: {
325
+ driver: {
326
+ name: 'Oculum',
327
+ version: '1.0.0',
328
+ informationUri: 'https://oculum.dev',
329
+ organization: 'Oculum Security',
330
+ rules: getUniqueRules(result.vulnerabilities),
331
+ },
332
+ },
333
+ results: result.vulnerabilities.map((v, index) => ({
334
+ ruleId: v.category,
335
+ ruleIndex: getRuleIndex(result.vulnerabilities, v.category),
336
+ level: mapSeverityToSARIF(v.severity),
337
+ message: {
338
+ text: v.description,
339
+ },
340
+ locations: [{
341
+ physicalLocation: {
342
+ artifactLocation: {
343
+ uri: v.filePath,
344
+ uriBaseId: '%SRCROOT%',
345
+ },
346
+ region: {
347
+ startLine: v.lineNumber,
348
+ startColumn: 1,
349
+ snippet: v.lineContent ? { text: v.lineContent } : undefined,
350
+ },
351
+ },
352
+ }],
353
+ fingerprints: {
354
+ 'oculum/v1': `${v.category}:${v.filePath}:${v.lineNumber}`,
355
+ },
356
+ fixes: v.suggestedFix ? [{
357
+ description: {
358
+ text: v.suggestedFix,
359
+ },
360
+ }] : undefined,
361
+ properties: {
362
+ confidence: v.confidence,
363
+ layer: v.layer,
364
+ },
365
+ })),
366
+ columnKind: 'utf16CodeUnits',
367
+ }],
368
+ }
369
+ }
370
+
371
+ function mapSeverityToSARIF(severity: VulnerabilitySeverity): 'error' | 'warning' | 'note' {
372
+ switch (severity) {
373
+ case 'critical':
374
+ case 'high':
375
+ return 'error'
376
+ case 'medium':
377
+ return 'warning'
378
+ default:
379
+ return 'note'
380
+ }
381
+ }
382
+
383
+ function getRuleIndex(vulnerabilities: Vulnerability[], category: string): number {
384
+ const seen = new Set<string>()
385
+ let index = 0
386
+ for (const v of vulnerabilities) {
387
+ if (!seen.has(v.category)) {
388
+ if (v.category === category) return index
389
+ seen.add(v.category)
390
+ index++
391
+ }
392
+ }
393
+ return 0
394
+ }
395
+
396
+ function getUniqueRules(vulnerabilities: Vulnerability[]): object[] {
397
+ const seen = new Set<string>()
398
+ const rules: object[] = []
399
+
400
+ for (const v of vulnerabilities) {
401
+ if (seen.has(v.category)) continue
402
+ seen.add(v.category)
403
+
404
+ const metadata = RULE_METADATA[v.category]
405
+ const ruleName = metadata?.name || v.category.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
406
+
407
+ rules.push({
408
+ id: v.category,
409
+ name: ruleName,
410
+ shortDescription: { text: ruleName },
411
+ fullDescription: {
412
+ text: metadata?.description || v.description,
413
+ },
414
+ helpUri: metadata?.helpUri || `https://oculum.dev/docs/rules/${v.category.replace(/_/g, '-')}`,
415
+ help: {
416
+ text: metadata?.description || v.description,
417
+ markdown: `# ${ruleName}\n\n${metadata?.description || v.description}\n\n[Learn more](${metadata?.helpUri || 'https://oculum.dev/docs'})`,
418
+ },
419
+ defaultConfiguration: {
420
+ level: mapSeverityToSARIF(v.severity),
421
+ },
422
+ properties: {
423
+ tags: metadata?.tags || ['security'],
424
+ precision: v.confidence === 'high' ? 'high' : v.confidence === 'medium' ? 'medium' : 'low',
425
+ 'security-severity': mapSeverityToScore(v.severity),
426
+ },
427
+ })
428
+ }
429
+
430
+ return rules
431
+ }
432
+
433
+ function mapSeverityToScore(severity: VulnerabilitySeverity): string {
434
+ switch (severity) {
435
+ case 'critical':
436
+ return '9.0'
437
+ case 'high':
438
+ return '7.0'
439
+ case 'medium':
440
+ return '5.0'
441
+ case 'low':
442
+ return '3.0'
443
+ default:
444
+ return '1.0'
445
+ }
446
+ }