@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,293 @@
1
+ /**
2
+ * Incremental Scan Mode
3
+ * Optimized scanning for PR workflows - only scan changed files and surface relevant findings
4
+ */
5
+
6
+ import type { ScanFile, Vulnerability, ScanResult, ScanModeConfig } from '../types'
7
+ import { runLayer1Scan } from '../layer1'
8
+ import { runLayer2Scan } from '../layer2'
9
+ import {
10
+ parseDiff,
11
+ parseChangedFileList,
12
+ filterToChangedLines,
13
+ getChangedFilePaths,
14
+ type FileDiff,
15
+ } from '../utils/diff-parser'
16
+ import { detectGlobalAuthMiddleware } from '../utils/middleware-detector'
17
+
18
+ /**
19
+ * Options for incremental scanning
20
+ */
21
+ export interface IncrementalScanOptions {
22
+ /** Git diff output (if available) */
23
+ diffContent?: string
24
+ /** List of changed file paths (alternative to diff) */
25
+ changedFiles?: string[]
26
+ /** Whether to only show findings on exactly changed lines (strict) or nearby (default) */
27
+ strictLineMatching?: boolean
28
+ /** Context window for "near changed line" (default: 5) */
29
+ contextWindow?: number
30
+ /** Whether to mark findings as "introduced in this PR" */
31
+ markAsIntroduced?: boolean
32
+ /** Previous findings to compare against (for suppressing pre-existing issues) */
33
+ previousFindings?: Vulnerability[]
34
+ }
35
+
36
+ /**
37
+ * Result of incremental scan with additional PR-specific metadata
38
+ */
39
+ export interface IncrementalScanResult {
40
+ /** All findings (filtered to changed lines) */
41
+ findings: Vulnerability[]
42
+ /** Findings that are new in this PR */
43
+ introduced: Vulnerability[]
44
+ /** Findings that existed before (if previousFindings provided) */
45
+ preExisting: Vulnerability[]
46
+ /** Number of files scanned */
47
+ filesScanned: number
48
+ /** Number of files that were changed */
49
+ filesChanged: number
50
+ /** Parsed diff information */
51
+ diffs: Map<string, FileDiff>
52
+ /** Scan duration in ms */
53
+ duration: number
54
+ }
55
+
56
+ /**
57
+ * Run an incremental scan optimized for PR workflows
58
+ *
59
+ * This scans:
60
+ * 1. All changed files (added + modified)
61
+ * 2. Files that import changed files (for context)
62
+ * 3. Middleware files (for auth context)
63
+ *
64
+ * And only surfaces findings on/near changed lines.
65
+ */
66
+ export async function runIncrementalScan(
67
+ allFiles: ScanFile[],
68
+ options: IncrementalScanOptions
69
+ ): Promise<IncrementalScanResult> {
70
+ const startTime = Date.now()
71
+
72
+ const {
73
+ diffContent,
74
+ changedFiles,
75
+ strictLineMatching = false,
76
+ contextWindow = 5,
77
+ markAsIntroduced = true,
78
+ previousFindings = [],
79
+ } = options
80
+
81
+ // Parse diff or file list to get changed files
82
+ let diffs: Map<string, FileDiff>
83
+
84
+ if (diffContent) {
85
+ diffs = parseDiff(diffContent, contextWindow)
86
+ } else if (changedFiles && changedFiles.length > 0) {
87
+ diffs = parseChangedFileList(changedFiles)
88
+ } else {
89
+ // No diff info - scan everything but don't filter
90
+ console.log('[Incremental] No diff info provided, scanning all files')
91
+ diffs = new Map()
92
+ }
93
+
94
+ const changedPaths = getChangedFilePaths(diffs)
95
+ console.log(`[Incremental] Changed files: ${changedPaths.length}`)
96
+
97
+ // Build file index for import resolution
98
+ const fileIndex = new Map<string, ScanFile>()
99
+ for (const file of allFiles) {
100
+ fileIndex.set(file.path, file)
101
+ }
102
+
103
+ // Determine which files to scan
104
+ const filesToScan: ScanFile[] = []
105
+ const scannedPaths = new Set<string>()
106
+
107
+ // 1. Add all changed files
108
+ for (const path of changedPaths) {
109
+ const file = fileIndex.get(path)
110
+ if (file && !scannedPaths.has(path)) {
111
+ filesToScan.push(file)
112
+ scannedPaths.add(path)
113
+ }
114
+ }
115
+
116
+ // 2. Add files that import changed files (for context)
117
+ // This helps detect issues where changes break dependencies
118
+ const importers = findImporters(allFiles, changedPaths)
119
+ for (const path of importers) {
120
+ if (!scannedPaths.has(path)) {
121
+ const file = fileIndex.get(path)
122
+ if (file) {
123
+ filesToScan.push(file)
124
+ scannedPaths.add(path)
125
+ }
126
+ }
127
+ }
128
+
129
+ // 3. Always include middleware files for auth context
130
+ const middlewareFile = allFiles.find(f =>
131
+ f.path.includes('middleware.ts') ||
132
+ f.path.includes('middleware.js')
133
+ )
134
+ if (middlewareFile && !scannedPaths.has(middlewareFile.path)) {
135
+ filesToScan.push(middlewareFile)
136
+ scannedPaths.add(middlewareFile.path)
137
+ }
138
+
139
+ console.log(`[Incremental] Scanning ${filesToScan.length} files (${changedPaths.length} changed + ${importers.size} importers)`)
140
+
141
+ // Detect auth middleware from ALL files (for context)
142
+ const middlewareConfig = detectGlobalAuthMiddleware(allFiles)
143
+
144
+ // Run Layer 1 + Layer 2 on selected files
145
+ const layer1Result = await runLayer1Scan(filesToScan)
146
+ const layer2Result = await runLayer2Scan(filesToScan, { middlewareConfig })
147
+
148
+ let allFindings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities]
149
+
150
+ // Filter to only findings on/near changed lines
151
+ if (diffs.size > 0) {
152
+ const beforeFilter = allFindings.length
153
+ allFindings = filterToChangedLines(allFindings, diffs, { strictMode: strictLineMatching })
154
+ console.log(`[Incremental] Filtered findings: ${beforeFilter} → ${allFindings.length} (on/near changed lines)`)
155
+ }
156
+
157
+ // Mark findings as introduced and separate pre-existing
158
+ const introduced: Vulnerability[] = []
159
+ const preExisting: Vulnerability[] = []
160
+
161
+ if (previousFindings.length > 0) {
162
+ // Create fingerprints for previous findings
163
+ const previousFingerprints = new Set(
164
+ previousFindings.map(f => `${f.filePath}:${f.lineNumber}:${f.category}`)
165
+ )
166
+
167
+ for (const finding of allFindings) {
168
+ const fingerprint = `${finding.filePath}:${finding.lineNumber}:${finding.category}`
169
+
170
+ if (previousFingerprints.has(fingerprint)) {
171
+ preExisting.push(finding)
172
+ } else {
173
+ if (markAsIntroduced) {
174
+ finding.validationNotes = (finding.validationNotes || '') + ' [Introduced in this PR]'
175
+ }
176
+ introduced.push(finding)
177
+ }
178
+ }
179
+ } else {
180
+ // No previous findings - all are "introduced"
181
+ introduced.push(...allFindings)
182
+ }
183
+
184
+ const duration = Date.now() - startTime
185
+ console.log(`[Incremental] Scan completed in ${duration}ms: ${introduced.length} new, ${preExisting.length} pre-existing`)
186
+
187
+ return {
188
+ findings: allFindings,
189
+ introduced,
190
+ preExisting,
191
+ filesScanned: filesToScan.length,
192
+ filesChanged: changedPaths.length,
193
+ diffs,
194
+ duration,
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Find files that import any of the changed files
200
+ */
201
+ function findImporters(allFiles: ScanFile[], changedPaths: string[]): Set<string> {
202
+ const importers = new Set<string>()
203
+
204
+ // Create patterns to match imports
205
+ const importPatterns = changedPaths.map(path => {
206
+ // Remove extension for import matching
207
+ const withoutExt = path.replace(/\.[^/.]+$/, '')
208
+ // Get just the filename without path for relative imports
209
+ const filename = withoutExt.split('/').pop() || ''
210
+ return { fullPath: withoutExt, filename }
211
+ })
212
+
213
+ for (const file of allFiles) {
214
+ // Skip if this file is already in changed paths
215
+ if (changedPaths.includes(file.path)) continue
216
+
217
+ // Check if this file imports any changed file
218
+ for (const { fullPath, filename } of importPatterns) {
219
+ // Match various import patterns
220
+ const importRegex = new RegExp(
221
+ `(?:import|require)\\s*(?:\\([^)]*|[^;]*from\\s*)['"]` +
222
+ `(?:\\.{0,2}/)?(?:${escapeRegex(fullPath)}|[^'"]*/${escapeRegex(filename)})['"]`,
223
+ 'i'
224
+ )
225
+
226
+ if (importRegex.test(file.content)) {
227
+ importers.add(file.path)
228
+ break
229
+ }
230
+ }
231
+ }
232
+
233
+ return importers
234
+ }
235
+
236
+ /**
237
+ * Escape special regex characters
238
+ */
239
+ function escapeRegex(str: string): string {
240
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
241
+ }
242
+
243
+ /**
244
+ * Create a PR-optimized scan config
245
+ */
246
+ export function createPRScanConfig(
247
+ changedFiles: string[],
248
+ options: Partial<ScanModeConfig> = {}
249
+ ): ScanModeConfig {
250
+ return {
251
+ mode: 'incremental',
252
+ changedFiles,
253
+ skipAIValidation: false, // Use AI for validation
254
+ skipLayer3: true, // Skip deep analysis for speed
255
+ maxAIValidationFiles: 20,
256
+ maxLayer3Files: 0,
257
+ scanDepth: 'cheap', // Fast feedback for PRs
258
+ ...options,
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Format incremental scan result for PR comment
264
+ */
265
+ export function formatIncrementalForPR(result: IncrementalScanResult): {
266
+ summary: string
267
+ hasNewIssues: boolean
268
+ blockingIssues: number
269
+ } {
270
+ const blocking = result.introduced.filter(
271
+ f => f.severity === 'critical' || f.severity === 'high'
272
+ )
273
+
274
+ let summary: string
275
+
276
+ if (result.introduced.length === 0) {
277
+ summary = `✅ No new security issues introduced in this PR`
278
+ } else if (blocking.length > 0) {
279
+ summary = `🚨 ${blocking.length} blocking issue${blocking.length === 1 ? '' : 's'} introduced`
280
+ } else {
281
+ summary = `⚠️ ${result.introduced.length} new issue${result.introduced.length === 1 ? '' : 's'} to review`
282
+ }
283
+
284
+ if (result.preExisting.length > 0) {
285
+ summary += ` (${result.preExisting.length} pre-existing)`
286
+ }
287
+
288
+ return {
289
+ summary,
290
+ hasNewIssues: result.introduced.length > 0,
291
+ blockingIssues: blocking.length,
292
+ }
293
+ }
package/src/tiers.ts ADDED
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Detector Tier System
3
+ *
4
+ * Provides a shared language for "how much we trust this detector" so we can:
5
+ * - Filter findings in runScan by tier + depth
6
+ * - Log tier breakdowns for tuning
7
+ * - Route AI validation budget toward Tier B
8
+ *
9
+ * Security reasoning:
10
+ * - Makes it explicit which detectors are safe to expose in cheap scans
11
+ * - Avoids "accidental promotion" of an experimental heuristic to production output
12
+ */
13
+
14
+ import type { VulnerabilityCategory } from './types'
15
+
16
+ /**
17
+ * Detector tiers control visibility and trust level:
18
+ *
19
+ * - core: High-precision SAST + core AI-safety detectors. Visible in all scan depths.
20
+ * - ai_assisted: Context-heavy heuristics that need AI validation. Shown in validated/deep.
21
+ * - experimental: High-noise signals used only for internal scoring/AI hints. Hidden from users.
22
+ */
23
+ export type DetectorTier = 'core' | 'ai_assisted' | 'experimental'
24
+
25
+ /**
26
+ * Tier statistics for logging and analysis
27
+ */
28
+ export interface TierStats {
29
+ core: number
30
+ ai_assisted: number
31
+ experimental: number
32
+ }
33
+
34
+ // ============================================================================
35
+ // Layer 1 Detector Tier Mappings
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Layer 1 detector names (internal identifiers matching detector function names)
40
+ */
41
+ export type Layer1DetectorName =
42
+ | 'known_secrets' // patterns.ts - Known secret patterns (API keys, tokens)
43
+ | 'weak_crypto' // weak-crypto.ts - Weak hash/cipher usage
44
+ | 'sensitive_urls' // urls.ts - Webhook URLs, URLs with tokens, internal endpoints
45
+ | 'entropy' // entropy.ts - High-entropy string detection
46
+ | 'config_audit' // config-audit.ts - Risky .env/config and debug flags
47
+ | 'file_flags' // file-flags.ts - Dangerous file patterns
48
+ | 'ai_comments' // comments.ts - AI comment patterns ("Generated by...")
49
+
50
+ /**
51
+ * Layer 1 tier assignments
52
+ *
53
+ * Tier A (core):
54
+ * - known_secrets: Hardcoded secrets are objectively bad and high-impact
55
+ * - weak_crypto: Weak crypto is a classic SAST finding with clear remediation
56
+ * - sensitive_urls: Hardcoded webhook URLs + tokens are real data exfil vectors
57
+ *
58
+ * Tier B (ai_assisted):
59
+ * - entropy: Great at finding candidates, needs AI to separate real secrets from noise
60
+ * - config_audit: Depends on project norms; better reviewed with AI + project context
61
+ * - file_flags: Subjective items should be AI-triaged (except committed .env = Tier A)
62
+ *
63
+ * Tier C (experimental):
64
+ * - ai_comments: Not directly a vuln; belongs in separate "AI hygiene" report
65
+ */
66
+ export const LAYER1_DETECTOR_TIERS: Record<Layer1DetectorName, DetectorTier> = {
67
+ known_secrets: 'core',
68
+ weak_crypto: 'core',
69
+ sensitive_urls: 'core',
70
+ entropy: 'ai_assisted',
71
+ config_audit: 'ai_assisted',
72
+ file_flags: 'ai_assisted', // Mixed: committed .env is effectively core
73
+ ai_comments: 'experimental',
74
+ }
75
+
76
+ /**
77
+ * Mapping from vulnerability category to Layer 1 detector name
78
+ * Used for tier lookups when we only have the category
79
+ */
80
+ export const LAYER1_CATEGORY_TO_DETECTOR: Partial<Record<VulnerabilityCategory, Layer1DetectorName>> = {
81
+ hardcoded_secret: 'known_secrets',
82
+ weak_crypto: 'weak_crypto',
83
+ sensitive_url: 'sensitive_urls',
84
+ high_entropy_string: 'entropy',
85
+ insecure_config: 'config_audit',
86
+ root_container: 'config_audit',
87
+ dangerous_file: 'file_flags',
88
+ ai_pattern: 'ai_comments', // AI comment patterns detected in Layer 1
89
+ }
90
+
91
+ // ============================================================================
92
+ // Layer 2 Detector Tier Mappings
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Layer 2 detector names (internal identifiers matching detector function names)
97
+ */
98
+ export type Layer2DetectorName =
99
+ | 'dangerous_functions' // dangerous-functions.ts
100
+ | 'byok_patterns' // byok-patterns.ts
101
+ | 'ai_execution_sinks' // ai-execution-sinks.ts
102
+ | 'ai_agent_tools' // ai-agent-tools.ts
103
+ | 'auth_antipatterns' // auth-antipatterns.ts
104
+ | 'data_exposure' // data-exposure.ts
105
+ | 'ai_fingerprinting' // ai-fingerprinting.ts
106
+ | 'ai_prompt_hygiene' // ai-prompt-hygiene.ts
107
+ | 'logic_gates' // logic-gates.ts
108
+ | 'variables' // variables.ts
109
+ | 'risky_imports' // risky-imports.ts
110
+ | 'framework_checks' // framework-checks.ts
111
+ // M5: New AI-era detectors
112
+ | 'ai_rag_safety' // ai-rag-safety.ts - RAG data exfiltration
113
+ | 'ai_endpoint_protection' // ai-endpoint-protection.ts - Unprotected AI endpoints
114
+ | 'ai_schema_validation' // ai-schema-validation.ts - Schema mismatch
115
+
116
+ /**
117
+ * Layer 2 tier assignments
118
+ *
119
+ * Tier A (core) - High-precision, high-risk, clear remediation:
120
+ * - dangerous_functions: Classic injection footholds (eval, exec, unsafe SQL)
121
+ * - byok_patterns: Storing/logging BYOK is critical AI-era customer trust risk
122
+ * - ai_execution_sinks: Core to Oculum's AI story; LLM output as code/commands
123
+ * - ai_agent_tools: Over-permissive tools are central AI alignment problem
124
+ *
125
+ * Tier B (ai_assisted) - Context-heavy, need AI validation:
126
+ * - auth_antipatterns: Very context-dependent; needs middleware awareness
127
+ * - data_exposure: Logging queries/user IDs is often acceptable but context-specific
128
+ * - ai_fingerprinting: TypeScript 'any' at trust boundaries is risk indicator, not vuln
129
+ * - ai_prompt_hygiene: Needs semantic understanding of prompt design
130
+ *
131
+ * Tier C (experimental) - High-noise, internal use only:
132
+ * - logic_gates: Many hits with limited actionable signal
133
+ * - variables: Generic token/password variable names - too generic
134
+ * - risky_imports: Most imports are fine; keep tiny whitelist if proved valuable
135
+ * - framework_checks: Many findings are style/low-risk; prone to noisy lint
136
+ */
137
+ export const LAYER2_DETECTOR_TIERS: Record<Layer2DetectorName, DetectorTier> = {
138
+ // Tier A - Core SAST / AI-safety
139
+ dangerous_functions: 'core',
140
+ byok_patterns: 'core',
141
+ ai_execution_sinks: 'core',
142
+ ai_agent_tools: 'core',
143
+
144
+ // Tier B - AI-assisted heuristics
145
+ auth_antipatterns: 'ai_assisted',
146
+ data_exposure: 'ai_assisted',
147
+ ai_fingerprinting: 'ai_assisted',
148
+ ai_prompt_hygiene: 'ai_assisted',
149
+
150
+ // Tier C - Experimental / high-noise
151
+ logic_gates: 'experimental',
152
+ variables: 'experimental',
153
+ risky_imports: 'experimental',
154
+ framework_checks: 'experimental',
155
+
156
+ // M5: New AI-era detectors
157
+ ai_rag_safety: 'core', // Tier A - Cross-tenant data access is critical
158
+ ai_endpoint_protection: 'core', // Tier A - Cost abuse / API exposure has clear signals
159
+ ai_schema_validation: 'ai_assisted', // Tier B - Context-dependent, benefits from AI validation
160
+ }
161
+
162
+ /**
163
+ * Mapping from vulnerability category to Layer 2 detector name
164
+ * Used for tier lookups when we only have the category
165
+ *
166
+ * NOTE: Some categories are used by multiple detectors:
167
+ * - ai_pattern: used by ai-fingerprinting (Tier B), byok-patterns (Tier A), dangerous-functions (Tier A)
168
+ * - insecure_config: used by config-audit (L1 Tier B), framework-checks (L2 Tier C)
169
+ *
170
+ * For ambiguous categories, we use the most conservative (highest trust) tier mapping.
171
+ * When category alone isn't sufficient, the orchestrator can use detector-specific tracking.
172
+ */
173
+ export const LAYER2_CATEGORY_TO_DETECTOR: Partial<Record<VulnerabilityCategory, Layer2DetectorName>> = {
174
+ // Tier A categories (unambiguous)
175
+ dangerous_function: 'dangerous_functions',
176
+ sql_injection: 'dangerous_functions',
177
+ command_injection: 'dangerous_functions',
178
+ ai_unsafe_execution: 'ai_execution_sinks',
179
+ ai_overpermissive_tool: 'ai_agent_tools',
180
+
181
+ // Tier B categories
182
+ missing_auth: 'auth_antipatterns',
183
+ data_exposure: 'data_exposure',
184
+ ai_prompt_injection: 'ai_prompt_hygiene',
185
+
186
+ // ai_pattern is ambiguous - used by multiple detectors with different tiers:
187
+ // - ai-fingerprinting (Tier B): TypeScript 'any' at boundaries
188
+ // - byok-patterns (Tier A): BYOK key handling
189
+ // - dangerous-functions (Tier A): JSON.parse related AI patterns
190
+ // Default to ai_fingerprinting (Tier B) since it's most common; byok/dangerous_functions
191
+ // findings are usually categorized differently or handled explicitly
192
+ ai_pattern: 'ai_fingerprinting',
193
+
194
+ // Tier C categories
195
+ security_bypass: 'logic_gates',
196
+ sensitive_variable: 'variables',
197
+ suspicious_package: 'risky_imports',
198
+ cors_misconfiguration: 'framework_checks',
199
+ // insecure_config from framework-checks is Tier C in Layer 2
200
+ // (but same category from config-audit is Tier B in Layer 1 - handled by layer check)
201
+ insecure_config: 'framework_checks',
202
+
203
+ // M5: New AI-era categories
204
+ ai_rag_exfiltration: 'ai_rag_safety',
205
+ ai_endpoint_unprotected: 'ai_endpoint_protection',
206
+ ai_schema_mismatch: 'ai_schema_validation',
207
+ }
208
+
209
+ // ============================================================================
210
+ // Tier Lookup Helpers
211
+ // ============================================================================
212
+
213
+ /**
214
+ * Get the tier for a vulnerability based on its category and layer
215
+ */
216
+ export function getTierForCategory(
217
+ category: VulnerabilityCategory,
218
+ layer: 1 | 2 | 3
219
+ ): DetectorTier {
220
+ if (layer === 1) {
221
+ const detector = LAYER1_CATEGORY_TO_DETECTOR[category]
222
+ if (detector) {
223
+ return LAYER1_DETECTOR_TIERS[detector]
224
+ }
225
+ } else if (layer === 2) {
226
+ const detector = LAYER2_CATEGORY_TO_DETECTOR[category]
227
+ if (detector) {
228
+ return LAYER2_DETECTOR_TIERS[detector]
229
+ }
230
+ }
231
+
232
+ // Layer 3 findings are always core (AI semantic analysis)
233
+ if (layer === 3) {
234
+ return 'core'
235
+ }
236
+
237
+ // Default to ai_assisted if unmapped (safe default - will go through AI validation)
238
+ return 'ai_assisted'
239
+ }
240
+
241
+ /**
242
+ * Get tier for a Layer 1 detector by name
243
+ */
244
+ export function getLayer1DetectorTier(detector: Layer1DetectorName): DetectorTier {
245
+ return LAYER1_DETECTOR_TIERS[detector]
246
+ }
247
+
248
+ /**
249
+ * Get tier for a Layer 2 detector by name
250
+ */
251
+ export function getLayer2DetectorTier(detector: Layer2DetectorName): DetectorTier {
252
+ return LAYER2_DETECTOR_TIERS[detector]
253
+ }
254
+
255
+ /**
256
+ * Check if a tier should be visible at a given scan depth
257
+ */
258
+ export function isTierVisibleAtDepth(
259
+ tier: DetectorTier,
260
+ depth: 'cheap' | 'validated' | 'deep'
261
+ ): boolean {
262
+ switch (depth) {
263
+ case 'cheap':
264
+ // Only Tier A (core) findings are visible in cheap scans
265
+ return tier === 'core'
266
+ case 'validated':
267
+ // Tier A always visible, Tier B visible after AI validation
268
+ return tier === 'core' || tier === 'ai_assisted'
269
+ case 'deep':
270
+ // Same as validated for visibility (deep adds Layer 3, not more tiers)
271
+ return tier === 'core' || tier === 'ai_assisted'
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Check if a tier should go through AI validation at a given scan depth
277
+ */
278
+ export function shouldValidateWithAI(
279
+ tier: DetectorTier,
280
+ depth: 'cheap' | 'validated' | 'deep'
281
+ ): boolean {
282
+ // Cheap scans skip AI validation entirely
283
+ if (depth === 'cheap') {
284
+ return false
285
+ }
286
+
287
+ // In validated/deep, Tier B findings should go through AI validation
288
+ // Tier A is high-precision and doesn't need AI validation
289
+ // Tier C is hidden anyway
290
+ return tier === 'ai_assisted'
291
+ }
292
+
293
+ /**
294
+ * Compute tier statistics from an array of vulnerabilities
295
+ */
296
+ export function computeTierStats(
297
+ vulnerabilities: Array<{ category: VulnerabilityCategory; layer: 1 | 2 | 3 }>
298
+ ): TierStats {
299
+ const stats: TierStats = {
300
+ core: 0,
301
+ ai_assisted: 0,
302
+ experimental: 0,
303
+ }
304
+
305
+ for (const vuln of vulnerabilities) {
306
+ const tier = getTierForCategory(vuln.category, vuln.layer)
307
+ stats[tier]++
308
+ }
309
+
310
+ return stats
311
+ }
312
+
313
+ /**
314
+ * Format tier stats for logging
315
+ */
316
+ export function formatTierStats(stats: TierStats): string {
317
+ return `tiers={core:${stats.core},ai_assisted:${stats.ai_assisted},experimental:${stats.experimental}}`
318
+ }