@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,272 @@
1
+ /**
2
+ * Git Diff Parser
3
+ * Parses unified diff format to extract changed line ranges
4
+ * Used for incremental scanning to only flag findings on changed lines
5
+ */
6
+
7
+ /**
8
+ * A hunk represents a contiguous block of changes
9
+ */
10
+ export interface DiffHunk {
11
+ /** Starting line in the old file */
12
+ oldStart: number
13
+ /** Number of lines in old file */
14
+ oldLines: number
15
+ /** Starting line in the new file */
16
+ newStart: number
17
+ /** Number of lines in new file */
18
+ newLines: number
19
+ /** Lines that were added (line numbers in new file) */
20
+ addedLines: number[]
21
+ /** Lines that were removed (line numbers in old file) */
22
+ removedLines: number[]
23
+ /** Lines that were modified (context around changes in new file) */
24
+ contextLines: number[]
25
+ }
26
+
27
+ /**
28
+ * Parsed file diff
29
+ */
30
+ export interface FileDiff {
31
+ /** File path (after rename if applicable) */
32
+ path: string
33
+ /** Old file path (if renamed) */
34
+ oldPath?: string
35
+ /** Whether the file was added */
36
+ isNew: boolean
37
+ /** Whether the file was deleted */
38
+ isDeleted: boolean
39
+ /** Whether the file was renamed */
40
+ isRenamed: boolean
41
+ /** Hunks containing the actual changes */
42
+ hunks: DiffHunk[]
43
+ /** All changed line numbers in the new file */
44
+ changedLines: Set<number>
45
+ /** All lines near changes (within context window) */
46
+ affectedLines: Set<number>
47
+ }
48
+
49
+ /**
50
+ * Parse a unified diff output
51
+ *
52
+ * @param diffOutput - The raw git diff output
53
+ * @param contextWindow - Lines around changes to consider "affected" (default: 5)
54
+ * @returns Map of file path to FileDiff
55
+ */
56
+ export function parseDiff(diffOutput: string, contextWindow: number = 5): Map<string, FileDiff> {
57
+ const files = new Map<string, FileDiff>()
58
+
59
+ if (!diffOutput || diffOutput.trim() === '') {
60
+ return files
61
+ }
62
+
63
+ // Split into file sections (each starts with "diff --git")
64
+ const fileSections = diffOutput.split(/^diff --git /gm).filter(s => s.trim())
65
+
66
+ for (const section of fileSections) {
67
+ const fileDiff = parseFileSection('diff --git ' + section, contextWindow)
68
+ if (fileDiff) {
69
+ files.set(fileDiff.path, fileDiff)
70
+ }
71
+ }
72
+
73
+ return files
74
+ }
75
+
76
+ /**
77
+ * Parse a single file's diff section
78
+ */
79
+ function parseFileSection(section: string, contextWindow: number): FileDiff | null {
80
+ const lines = section.split('\n')
81
+
82
+ // Extract file paths from header
83
+ // Format: diff --git a/path/to/file b/path/to/file
84
+ const headerMatch = lines[0].match(/diff --git a\/(.+) b\/(.+)/)
85
+ if (!headerMatch) return null
86
+
87
+ const oldPath = headerMatch[1]
88
+ const newPath = headerMatch[2]
89
+
90
+ // Detect file status
91
+ let isNew = false
92
+ let isDeleted = false
93
+ let isRenamed = oldPath !== newPath
94
+
95
+ for (const line of lines.slice(0, 10)) {
96
+ if (line.startsWith('new file mode')) isNew = true
97
+ if (line.startsWith('deleted file mode')) isDeleted = true
98
+ if (line.startsWith('rename from')) isRenamed = true
99
+ }
100
+
101
+ // Parse hunks
102
+ const hunks: DiffHunk[] = []
103
+ const changedLines = new Set<number>()
104
+ const affectedLines = new Set<number>()
105
+
106
+ // Find hunk headers: @@ -oldStart,oldLines +newStart,newLines @@
107
+ const hunkRegex = /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/g
108
+ let match: RegExpExecArray | null
109
+
110
+ const sectionContent = section
111
+
112
+ while ((match = hunkRegex.exec(sectionContent)) !== null) {
113
+ const oldStart = parseInt(match[1], 10)
114
+ const oldLines = parseInt(match[2] || '1', 10)
115
+ const newStart = parseInt(match[3], 10)
116
+ const newLines = parseInt(match[4] || '1', 10)
117
+
118
+ // Find the content of this hunk (until next @@ or end)
119
+ const hunkStartIndex = match.index + match[0].length
120
+ const nextHunkMatch = /@@ -\d+/.exec(sectionContent.slice(hunkStartIndex + 1))
121
+ const hunkEndIndex = nextHunkMatch
122
+ ? hunkStartIndex + 1 + nextHunkMatch.index
123
+ : sectionContent.length
124
+
125
+ const hunkContent = sectionContent.slice(hunkStartIndex, hunkEndIndex)
126
+ const hunkLines = hunkContent.split('\n')
127
+
128
+ const addedLines: number[] = []
129
+ const removedLines: number[] = []
130
+ const contextLines: number[] = []
131
+
132
+ let newLineNum = newStart
133
+ let oldLineNum = oldStart
134
+
135
+ for (const line of hunkLines) {
136
+ if (line.startsWith('+') && !line.startsWith('+++')) {
137
+ // Added line
138
+ addedLines.push(newLineNum)
139
+ changedLines.add(newLineNum)
140
+ newLineNum++
141
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
142
+ // Removed line
143
+ removedLines.push(oldLineNum)
144
+ oldLineNum++
145
+ } else if (line.startsWith(' ') || line === '') {
146
+ // Context line
147
+ contextLines.push(newLineNum)
148
+ newLineNum++
149
+ oldLineNum++
150
+ }
151
+ }
152
+
153
+ hunks.push({
154
+ oldStart,
155
+ oldLines,
156
+ newStart,
157
+ newLines,
158
+ addedLines,
159
+ removedLines,
160
+ contextLines,
161
+ })
162
+ }
163
+
164
+ // Calculate affected lines (changed lines + context window)
165
+ for (const line of changedLines) {
166
+ for (let i = Math.max(1, line - contextWindow); i <= line + contextWindow; i++) {
167
+ affectedLines.add(i)
168
+ }
169
+ }
170
+
171
+ return {
172
+ path: newPath,
173
+ oldPath: isRenamed ? oldPath : undefined,
174
+ isNew,
175
+ isDeleted,
176
+ isRenamed,
177
+ hunks,
178
+ changedLines,
179
+ affectedLines,
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Parse a simple list of changed file paths
185
+ * Used when full diff isn't available (e.g., GitHub API file list)
186
+ */
187
+ export function parseChangedFileList(files: string[]): Map<string, FileDiff> {
188
+ const result = new Map<string, FileDiff>()
189
+
190
+ for (const path of files) {
191
+ result.set(path, {
192
+ path,
193
+ isNew: false,
194
+ isDeleted: false,
195
+ isRenamed: false,
196
+ hunks: [],
197
+ // Without diff content, consider all lines as changed
198
+ changedLines: new Set(),
199
+ affectedLines: new Set(),
200
+ })
201
+ }
202
+
203
+ return result
204
+ }
205
+
206
+ /**
207
+ * Check if a finding is on a changed line
208
+ */
209
+ export function isOnChangedLine(
210
+ filePath: string,
211
+ lineNumber: number,
212
+ diffs: Map<string, FileDiff>
213
+ ): boolean {
214
+ const fileDiff = diffs.get(filePath)
215
+ if (!fileDiff) return false
216
+
217
+ // If no specific changed lines (file list only), consider all findings relevant
218
+ if (fileDiff.changedLines.size === 0) return true
219
+
220
+ return fileDiff.changedLines.has(lineNumber)
221
+ }
222
+
223
+ /**
224
+ * Check if a finding is near a changed line (within context window)
225
+ */
226
+ export function isNearChangedLine(
227
+ filePath: string,
228
+ lineNumber: number,
229
+ diffs: Map<string, FileDiff>
230
+ ): boolean {
231
+ const fileDiff = diffs.get(filePath)
232
+ if (!fileDiff) return false
233
+
234
+ // If no specific affected lines, consider all findings relevant
235
+ if (fileDiff.affectedLines.size === 0) return true
236
+
237
+ return fileDiff.affectedLines.has(lineNumber)
238
+ }
239
+
240
+ /**
241
+ * Get all changed file paths from diffs
242
+ */
243
+ export function getChangedFilePaths(diffs: Map<string, FileDiff>): string[] {
244
+ return Array.from(diffs.keys()).filter(path => {
245
+ const diff = diffs.get(path)
246
+ return diff && !diff.isDeleted
247
+ })
248
+ }
249
+
250
+ /**
251
+ * Filter vulnerabilities to only those on/near changed lines
252
+ */
253
+ export function filterToChangedLines<T extends { filePath: string; lineNumber: number }>(
254
+ findings: T[],
255
+ diffs: Map<string, FileDiff>,
256
+ options: { strictMode?: boolean } = {}
257
+ ): T[] {
258
+ const { strictMode = false } = options
259
+
260
+ return findings.filter(finding => {
261
+ // If file not in diff, it wasn't changed - exclude
262
+ if (!diffs.has(finding.filePath)) return false
263
+
264
+ // In strict mode, only include findings on exactly changed lines
265
+ if (strictMode) {
266
+ return isOnChangedLine(finding.filePath, finding.lineNumber, diffs)
267
+ }
268
+
269
+ // In normal mode, include findings near changed lines
270
+ return isNearChangedLine(finding.filePath, finding.lineNumber, diffs)
271
+ })
272
+ }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Imported Auth Detector
3
+ *
4
+ * Detects auth middleware/helpers imported from other files to avoid
5
+ * false positives on routes that are actually protected via imports.
6
+ *
7
+ * Example: A route file that does `import { authMiddleware } from '@/lib/auth'`
8
+ * and wraps handlers with it should not be flagged as "missing auth".
9
+ */
10
+
11
+ import type { ScanFile } from '../types'
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ export interface ImportedAuthInfo {
18
+ importPath: string // '@/lib/auth' or './middleware'
19
+ importedNames: string[] // ['authMiddleware', 'requireAuth']
20
+ isAuthRelated: boolean // Based on name patterns
21
+ }
22
+
23
+ export interface FileAuthImports {
24
+ filePath: string
25
+ imports: ImportedAuthInfo[]
26
+ usesImportedAuth: boolean // Does the file actually use imported auth?
27
+ }
28
+
29
+ // ============================================================================
30
+ // Patterns
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Patterns that indicate auth-related imports by name
35
+ */
36
+ const AUTH_NAME_PATTERNS = [
37
+ /auth/i,
38
+ /middleware/i,
39
+ /protect/i,
40
+ /guard/i,
41
+ /session/i,
42
+ /verify/i,
43
+ /secure/i,
44
+ /jwt/i,
45
+ /token/i,
46
+ ]
47
+
48
+ /**
49
+ * Specific auth import names commonly used
50
+ */
51
+ const KNOWN_AUTH_IMPORTS = new Set([
52
+ // Next.js / NextAuth
53
+ 'auth',
54
+ 'getServerSession',
55
+ 'getSession',
56
+ 'withAuth',
57
+ 'getToken',
58
+ 'NextAuth',
59
+ 'NextAuthOptions',
60
+
61
+ // Clerk
62
+ 'auth',
63
+ 'currentUser',
64
+ 'clerkMiddleware',
65
+ 'getAuth',
66
+ 'SignedIn',
67
+ 'SignedOut',
68
+
69
+ // Auth0
70
+ 'withPageAuthRequired',
71
+ 'withApiAuthRequired',
72
+ 'getSession',
73
+ 'getAccessToken',
74
+
75
+ // Custom auth patterns
76
+ 'authMiddleware',
77
+ 'requireAuth',
78
+ 'requireAuthentication',
79
+ 'checkAuth',
80
+ 'verifyAuth',
81
+ 'protectRoute',
82
+ 'withAuthentication',
83
+ 'authenticated',
84
+ 'getCurrentUser',
85
+ 'getCurrentUserId',
86
+ 'getUser',
87
+ 'getUserId',
88
+ ])
89
+
90
+ /**
91
+ * Patterns indicating auth-related import paths
92
+ */
93
+ const AUTH_PATH_PATTERNS = [
94
+ /\/auth\//i,
95
+ /\/middleware/i,
96
+ /\/lib\/auth/i,
97
+ /\/utils\/auth/i,
98
+ /\/helpers\/auth/i,
99
+ /next-auth/i,
100
+ /@clerk/i,
101
+ /@auth0/i,
102
+ /lucia/i,
103
+ ]
104
+
105
+ // ============================================================================
106
+ // Import Extraction
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Extract all imports from a file's content
111
+ */
112
+ export function extractImports(content: string): ImportedAuthInfo[] {
113
+ const imports: ImportedAuthInfo[] = []
114
+
115
+ // ES6 named imports: import { x, y } from 'path'
116
+ const es6NamedPattern = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g
117
+ let match
118
+
119
+ while ((match = es6NamedPattern.exec(content)) !== null) {
120
+ const names = match[1]
121
+ .split(',')
122
+ .map(n => n.trim().split(/\s+as\s+/)[0].trim()) // Handle 'x as y'
123
+ .filter(n => n.length > 0)
124
+ const importPath = match[2]
125
+
126
+ const isAuthRelated = isAuthRelatedImport(names, importPath)
127
+
128
+ imports.push({
129
+ importPath,
130
+ importedNames: names,
131
+ isAuthRelated,
132
+ })
133
+ }
134
+
135
+ // ES6 default imports: import x from 'path'
136
+ const defaultPattern = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g
137
+ while ((match = defaultPattern.exec(content)) !== null) {
138
+ const name = match[1]
139
+ const importPath = match[2]
140
+
141
+ // Skip if already captured as named import
142
+ if (imports.some(imp => imp.importPath === importPath)) continue
143
+
144
+ const isAuthRelated = isAuthRelatedImport([name], importPath)
145
+
146
+ imports.push({
147
+ importPath,
148
+ importedNames: [name],
149
+ isAuthRelated,
150
+ })
151
+ }
152
+
153
+ // CommonJS require: const { x } = require('path')
154
+ const requireDestructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
155
+ while ((match = requireDestructurePattern.exec(content)) !== null) {
156
+ const names = match[1]
157
+ .split(',')
158
+ .map(n => n.trim().split(/\s*:\s*/)[0].trim()) // Handle 'x: y' renaming
159
+ .filter(n => n.length > 0)
160
+ const importPath = match[2]
161
+
162
+ const isAuthRelated = isAuthRelatedImport(names, importPath)
163
+
164
+ imports.push({
165
+ importPath,
166
+ importedNames: names,
167
+ isAuthRelated,
168
+ })
169
+ }
170
+
171
+ // CommonJS require: const x = require('path')
172
+ const requireDefaultPattern = /(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
173
+ while ((match = requireDefaultPattern.exec(content)) !== null) {
174
+ const name = match[1]
175
+ const importPath = match[2]
176
+
177
+ // Skip if already captured
178
+ if (imports.some(imp => imp.importPath === importPath)) continue
179
+
180
+ const isAuthRelated = isAuthRelatedImport([name], importPath)
181
+
182
+ imports.push({
183
+ importPath,
184
+ importedNames: [name],
185
+ isAuthRelated,
186
+ })
187
+ }
188
+
189
+ return imports
190
+ }
191
+
192
+ /**
193
+ * Check if an import is auth-related based on names and path
194
+ */
195
+ function isAuthRelatedImport(names: string[], importPath: string): boolean {
196
+ // Check if path is auth-related
197
+ if (AUTH_PATH_PATTERNS.some(p => p.test(importPath))) {
198
+ return true
199
+ }
200
+
201
+ // Check if any imported name is auth-related
202
+ for (const name of names) {
203
+ // Known auth imports
204
+ if (KNOWN_AUTH_IMPORTS.has(name)) {
205
+ return true
206
+ }
207
+
208
+ // Pattern-based detection
209
+ if (AUTH_NAME_PATTERNS.some(p => p.test(name))) {
210
+ return true
211
+ }
212
+ }
213
+
214
+ return false
215
+ }
216
+
217
+ // ============================================================================
218
+ // Auth Usage Detection
219
+ // ============================================================================
220
+
221
+ /**
222
+ * Check if imported auth is actually used to protect routes/handlers
223
+ */
224
+ export function detectImportedAuthUsage(
225
+ content: string,
226
+ authImports: ImportedAuthInfo[]
227
+ ): boolean {
228
+ if (authImports.length === 0) return false
229
+
230
+ const authNames = authImports.flatMap(imp => imp.importedNames)
231
+
232
+ for (const name of authNames) {
233
+ // Pattern 1: Handler wrapping - export const GET = authMiddleware(...)
234
+ const wrapperPattern = new RegExp(
235
+ `(?:export\\s+(?:const|function)\\s+(?:GET|POST|PUT|PATCH|DELETE|handler)\\s*=\\s*${escapeRegex(name)}\\s*\\()` +
236
+ `|(?:${escapeRegex(name)}\\s*\\(\\s*(?:async\\s+)?(?:function|\\())`
237
+ )
238
+ if (wrapperPattern.test(content)) {
239
+ return true
240
+ }
241
+
242
+ // Pattern 2: Middleware chain - app.use(authMiddleware)
243
+ const middlewareChainPattern = new RegExp(
244
+ `\\.(?:use|all|get|post|put|patch|delete)\\s*\\([^)]*${escapeRegex(name)}`
245
+ )
246
+ if (middlewareChainPattern.test(content)) {
247
+ return true
248
+ }
249
+
250
+ // Pattern 3: Express/Fastify route with middleware - router.get('/', requireAuth, handler)
251
+ const routeMiddlewarePattern = new RegExp(
252
+ `\\.(?:get|post|put|patch|delete)\\s*\\([^,]+,\\s*${escapeRegex(name)}`
253
+ )
254
+ if (routeMiddlewarePattern.test(content)) {
255
+ return true
256
+ }
257
+
258
+ // Pattern 4: HOC pattern - withAuth(Component)
259
+ const hocPattern = new RegExp(`${escapeRegex(name)}\\s*\\(\\s*\\w+\\s*\\)`)
260
+ if (hocPattern.test(content)) {
261
+ return true
262
+ }
263
+
264
+ // Pattern 5: Auth function call at top of handler - const session = await auth()
265
+ const authCallPattern = new RegExp(
266
+ `(?:await\\s+)?${escapeRegex(name)}\\s*\\(\\s*\\)` +
267
+ `|${escapeRegex(name)}\\s*\\(\\s*(?:req|request|ctx|context)\\s*[,)]`
268
+ )
269
+ if (authCallPattern.test(content)) {
270
+ return true
271
+ }
272
+ }
273
+
274
+ return false
275
+ }
276
+
277
+ /**
278
+ * Escape special regex characters in a string
279
+ */
280
+ function escapeRegex(str: string): string {
281
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
282
+ }
283
+
284
+ // ============================================================================
285
+ // Registry Builder
286
+ // ============================================================================
287
+
288
+ /**
289
+ * Build a registry of files and their auth imports
290
+ */
291
+ export function buildFileAuthImports(files: ScanFile[]): Map<string, FileAuthImports> {
292
+ const registry = new Map<string, FileAuthImports>()
293
+
294
+ for (const file of files) {
295
+ const allImports = extractImports(file.content)
296
+ const authImports = allImports.filter(imp => imp.isAuthRelated)
297
+
298
+ const usesImportedAuth =
299
+ authImports.length > 0 &&
300
+ detectImportedAuthUsage(file.content, authImports)
301
+
302
+ registry.set(file.path, {
303
+ filePath: file.path,
304
+ imports: authImports,
305
+ usesImportedAuth,
306
+ })
307
+ }
308
+
309
+ return registry
310
+ }
311
+
312
+ /**
313
+ * Check if a specific file uses imported auth protection
314
+ */
315
+ export function fileUsesImportedAuth(
316
+ filePath: string,
317
+ registry: Map<string, FileAuthImports>
318
+ ): boolean {
319
+ return registry.get(filePath)?.usesImportedAuth ?? false
320
+ }