@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,535 @@
1
+ /**
2
+ * Shared Context Helpers
3
+ * Centralized utility functions for detecting file and code context
4
+ * Used across Layer 1 and Layer 2 scanners to reduce false positives
5
+ */
6
+
7
+ // ============================================================================
8
+ // File Path Context Detection
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Check if file is server-only (not bundled to client)
13
+ * Server-only files can safely use service role keys and other admin secrets
14
+ */
15
+ export function isServerOnlyFile(filePath: string): boolean {
16
+ const serverPatterns = [
17
+ /lib\/supabase\/(server|admin|middleware)\.(ts|js)$/i,
18
+ /\/api\//i, // Next.js API routes
19
+ /\/server\//i, // Server directories
20
+ /\.server\.(ts|js|tsx|jsx)$/i, // .server.ts files
21
+ /\/actions\//i, // Server actions
22
+ /middleware\.(ts|js)$/i, // Middleware files
23
+ /\/cron\//i, // Cron jobs
24
+ /\/workers?\//i, // Worker files
25
+ /\/scripts?\//i, // Scripts
26
+ /\/seed\//i, // Database seeds
27
+ /\/migrations?\//i, // Database migrations
28
+ /\/lib\/[^/]+\/server/i, // lib/*/server patterns
29
+ /\/utils\/server/i, // utils/server
30
+ /\/helpers\/server/i, // helpers/server
31
+ /\.action\.(ts|js)$/i, // .action.ts files
32
+ /route\.(ts|js)$/i, // Next.js route handlers
33
+ ]
34
+ return serverPatterns.some(pattern => pattern.test(filePath))
35
+ }
36
+
37
+ /**
38
+ * Check if file is a test, mock, or fixture file
39
+ * These files often contain fake secrets and should have lower severity
40
+ */
41
+ export function isTestOrMockFile(filePath: string): boolean {
42
+ const testPatterns = [
43
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/i,
44
+ /\/__tests__\//i,
45
+ /\/test\//i,
46
+ /\/tests\//i,
47
+ /\/mock/i,
48
+ /\/mocks\//i,
49
+ /\/fixtures?\//i,
50
+ /\.mock\.(ts|tsx|js|jsx)$/i,
51
+ /\.stub\.(ts|tsx|js|jsx)$/i,
52
+ /\.(stories|story)\.(ts|tsx|js|jsx)$/i, // Storybook
53
+ /\/e2e\//i, // E2E tests
54
+ /\/cypress\//i, // Cypress tests
55
+ /\/playwright\//i, // Playwright tests
56
+ /\/vitest\//i, // Vitest
57
+ /\/jest\//i, // Jest
58
+ ]
59
+ return testPatterns.some(pattern => pattern.test(filePath))
60
+ }
61
+
62
+ /**
63
+ * Check if file is an example/sample/template file
64
+ * These files should be skipped or have significantly reduced severity
65
+ */
66
+ export function isExampleFile(filePath: string): boolean {
67
+ return (
68
+ filePath.includes('.example') ||
69
+ filePath.includes('.sample') ||
70
+ filePath.includes('.template') ||
71
+ filePath.includes('README') ||
72
+ filePath.includes('/examples/') ||
73
+ filePath.includes('/example/') ||
74
+ filePath.includes('/demo/') ||
75
+ filePath.includes('/demos/')
76
+ )
77
+ }
78
+
79
+ /**
80
+ * Check if file is in an examples/demo directory
81
+ * Stronger check than isExampleFile - specifically for directories
82
+ * These are typically tutorial/demo code, not production patterns
83
+ */
84
+ export function isExampleDirectory(filePath: string): boolean {
85
+ const examplePatterns = [
86
+ /\/examples?\//i,
87
+ /\/demos?\//i,
88
+ /\/templates?\//i,
89
+ /\/samples?\//i,
90
+ /\/tutorials?\//i,
91
+ /\/cookbook\//i,
92
+ /\/quickstart\//i,
93
+ /\/getting-started\//i,
94
+ ]
95
+ return examplePatterns.some(pattern => pattern.test(filePath))
96
+ }
97
+
98
+ /**
99
+ * Check if file is library/framework code (base classes, utilities)
100
+ * Library code is intentionally generic - consumers add security
101
+ * This applies to: langchain, vercel/ai, llamaindex, etc.
102
+ */
103
+ export function isLibraryCode(filePath: string): boolean {
104
+ const libraryPatterns = [
105
+ // Package source directories in monorepos
106
+ /\/libs\/[^/]+\/src\//i,
107
+ /\/packages\/[^/]+\/src\//i,
108
+ // Common library patterns
109
+ /\/langchain-/i,
110
+ /\/llamaindex/i,
111
+ // Source files that aren't examples or tests
112
+ /\/src\/(?!.*(?:examples?|demos?|tests?)\/).*\.(ts|js)$/i,
113
+ ]
114
+
115
+ // Must match library pattern AND not be example/test
116
+ return (
117
+ libraryPatterns.some(pattern => pattern.test(filePath)) &&
118
+ !isExampleDirectory(filePath) &&
119
+ !isTestOrMockFile(filePath)
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Check if file is a fixture file (test data, mock responses)
125
+ * Fixtures contain fake data and should have reduced severity
126
+ */
127
+ export function isFixtureFile(filePath: string): boolean {
128
+ const fixturePatterns = [
129
+ /__fixtures__\//i,
130
+ /\.fixture\./i,
131
+ /fixtures?\//i,
132
+ /testdata\//i,
133
+ /test-data\//i,
134
+ /test_data\//i,
135
+ /mock-data\//i,
136
+ /mockdata\//i,
137
+ /\.mock\./i,
138
+ /\.stub\./i,
139
+ ]
140
+ return fixturePatterns.some(pattern => pattern.test(filePath))
141
+ }
142
+
143
+ /**
144
+ * Check if file is documentation (README, CHANGELOG, etc.)
145
+ * These files should typically be skipped for security scanning
146
+ */
147
+ export function isDocumentationFile(filePath: string): boolean {
148
+ const docPatterns = [
149
+ /README/i,
150
+ /CHANGELOG/i,
151
+ /CONTRIBUTING/i,
152
+ /LICENSE/i,
153
+ /\.md$/i,
154
+ /\.mdx$/i,
155
+ /\/docs\//i,
156
+ /\/documentation\//i,
157
+ ]
158
+ return docPatterns.some(pattern => pattern.test(filePath))
159
+ }
160
+
161
+ /**
162
+ * Check if file is scanner code, fixture, or rule definition
163
+ * Avoid flagging the scanner's own code/test cases
164
+ */
165
+ export function isScannerOrFixtureFile(filePath: string): boolean {
166
+ const scannerPatterns = [
167
+ /\/scanner\//i,
168
+ /\/detector\//i,
169
+ /\/security\//i,
170
+ /\/rules?\//i,
171
+ /\/patterns?\//i,
172
+ /\/fixtures?\//i,
173
+ /\/testdata\//i,
174
+ /\/test-data\//i,
175
+ /\/test_data\//i,
176
+ ]
177
+ return scannerPatterns.some(pattern => pattern.test(filePath))
178
+ }
179
+
180
+ /**
181
+ * Check if file is likely client-bundled (exposed to browser)
182
+ */
183
+ export function isClientBundledFile(filePath: string): boolean {
184
+ // Files in these locations are typically client-bundled
185
+ const clientPatterns = [
186
+ /\/components\//i,
187
+ /\/pages\//i, // Next.js pages (can be SSR, but code visible)
188
+ /\/app\/.*page\.(ts|tsx|js|jsx)$/i, // Next.js app router pages
189
+ /\/hooks\//i,
190
+ /\/contexts?\//i,
191
+ /\/providers?\//i,
192
+ /\/stores?\//i, // State management
193
+ /\.client\.(ts|js|tsx|jsx)$/i, // .client.ts files
194
+ ]
195
+
196
+ // But not if they're also server files
197
+ if (isServerOnlyFile(filePath)) {
198
+ return false
199
+ }
200
+
201
+ return clientPatterns.some(pattern => pattern.test(filePath))
202
+ }
203
+
204
+ // ============================================================================
205
+ // Code Line Context Detection
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Check if line uses environment variable reference (not hardcoded)
210
+ */
211
+ export function isEnvVarReference(line: string): boolean {
212
+ return (
213
+ /process\.env\.[A-Z_]+/.test(line) ||
214
+ /\$\{?[A-Z_]+\}?/.test(line) ||
215
+ /import\.meta\.env\.[A-Z_]+/.test(line) ||
216
+ /Deno\.env\.get\(/.test(line) ||
217
+ /os\.environ\[/.test(line) || // Python
218
+ /os\.getenv\(/.test(line) || // Python
219
+ /ENV\[['"]/.test(line) || // Ruby
220
+ /env\(["']/.test(line) // Laravel PHP
221
+ )
222
+ }
223
+
224
+ /**
225
+ * Check if line uses NEXT_PUBLIC_ prefix (client-exposed)
226
+ */
227
+ export function isNextPublicEnvVar(line: string): boolean {
228
+ return /NEXT_PUBLIC_[A-Z_]+/.test(line)
229
+ }
230
+
231
+ /**
232
+ * Check if line is a comment
233
+ */
234
+ export function isComment(lineContent: string): boolean {
235
+ const trimmed = lineContent.trim()
236
+ return (
237
+ trimmed.startsWith('//') ||
238
+ trimmed.startsWith('#') ||
239
+ trimmed.startsWith('*') ||
240
+ trimmed.startsWith('/*') ||
241
+ trimmed.startsWith('"""') ||
242
+ trimmed.startsWith("'''") ||
243
+ trimmed.startsWith('<!--')
244
+ )
245
+ }
246
+
247
+ /**
248
+ * Check if value/line appears to be a placeholder
249
+ */
250
+ export function isPlaceholderValue(value: string, line: string): boolean {
251
+ const placeholderPatterns = [
252
+ /xxx/i,
253
+ /your[-_]?/i,
254
+ /YOUR[-_]?/i,
255
+ /placeholder/i,
256
+ /example/i,
257
+ /REPLACE[-_]?/i,
258
+ /CHANGEME/i,
259
+ /<[a-z_-]+>/i, // <your-api-key>
260
+ /\[\s*[a-z_-]+\s*\]/i, // [API_KEY]
261
+ /todo/i,
262
+ /fixme/i,
263
+ ]
264
+
265
+ return placeholderPatterns.some(pattern =>
266
+ pattern.test(value) || pattern.test(line)
267
+ )
268
+ }
269
+
270
+ // ============================================================================
271
+ // Security Context Detection
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Check if line/path indicates a public endpoint (health, webhook, cron)
276
+ * These don't need authentication
277
+ */
278
+ export function isPublicEndpoint(lineContent: string, filePath: string): boolean {
279
+ // Health check patterns
280
+ const healthCheckPatterns = [
281
+ /\/health\/?["'`]?/i,
282
+ /\/healthz\/?["'`]?/i,
283
+ /\/ready\/?["'`]?/i,
284
+ /\/readyz\/?["'`]?/i,
285
+ /\/live\/?["'`]?/i,
286
+ /\/livez\/?["'`]?/i,
287
+ /\/ping\/?["'`]?/i,
288
+ /\/status\/?["'`]?/i,
289
+ /\/api\/health/i,
290
+ /\/api\/status/i,
291
+ /\/_health/i,
292
+ ]
293
+
294
+ // Webhook patterns
295
+ const webhookPatterns = [
296
+ /\/webhook/i,
297
+ /\/webhooks\//i,
298
+ /\/callback/i,
299
+ /\/stripe\/webhook/i,
300
+ /\/github\/webhook/i,
301
+ /\/clerk\/webhook/i,
302
+ ]
303
+
304
+ // Cron/scheduled job patterns
305
+ const cronPatterns = [
306
+ /\/cron\//i,
307
+ /\/scheduled\//i,
308
+ /\/tasks?\//i,
309
+ /\/jobs?\//i,
310
+ ]
311
+
312
+ // Check line content
313
+ const allPatterns = [...healthCheckPatterns, ...webhookPatterns, ...cronPatterns]
314
+ if (allPatterns.some(pattern => pattern.test(lineContent))) {
315
+ return true
316
+ }
317
+
318
+ // Check file path
319
+ if (filePath.includes('/health') ||
320
+ filePath.includes('/webhook') ||
321
+ filePath.includes('/cron') ||
322
+ filePath.includes('/scheduled')) {
323
+ return true
324
+ }
325
+
326
+ return false
327
+ }
328
+
329
+ /**
330
+ * Check if webhook has signature verification nearby
331
+ */
332
+ export function hasWebhookSignatureVerification(lines: string[], lineIndex: number, windowSize: number = 15): boolean {
333
+ const signaturePatterns = [
334
+ /verifySignature/i,
335
+ /validateSignature/i,
336
+ /checkSignature/i,
337
+ /signature.*verify/i,
338
+ /verify.*signature/i,
339
+ /hmac/i,
340
+ /x-hub-signature/i,
341
+ /stripe-signature/i,
342
+ /svix-signature/i,
343
+ /webhook.*secret/i,
344
+ /constructEvent/i, // Stripe webhook verification
345
+ /Webhook\.verify/i, // Generic webhook verify
346
+ ]
347
+
348
+ const start = Math.max(0, lineIndex - windowSize)
349
+ const end = Math.min(lines.length, lineIndex + windowSize)
350
+
351
+ for (let i = start; i < end; i++) {
352
+ if (signaturePatterns.some(pattern => pattern.test(lines[i]))) {
353
+ return true
354
+ }
355
+ }
356
+
357
+ return false
358
+ }
359
+
360
+ /**
361
+ * Check if there's an auth check nearby (bidirectional search)
362
+ */
363
+ export function hasAuthCheckNearby(lines: string[], lineIndex: number, windowSize: number = 20): boolean {
364
+ const authPatterns = [
365
+ /authorization/i,
366
+ /bearer\s+token/i,
367
+ /req\.user/i,
368
+ /request\.user/i,
369
+ /\.user\s*[=!]/,
370
+ /isAuthenticated/i,
371
+ /requireAuth/i,
372
+ /ensureAuth/i,
373
+ /checkAuth/i,
374
+ /verifyToken/i,
375
+ /validateToken/i,
376
+ /checkPermission/i,
377
+ /getServerSession/i,
378
+ /middleware.*auth/i,
379
+ /session\.user/i,
380
+ /currentUser/i,
381
+ /getSession\(/i,
382
+ /useSession\(/i,
383
+ /auth\(\)/i, // Next-Auth auth()
384
+ /withAuth/i,
385
+ /protected/i,
386
+ /verifySignature/i, // Webhook signature
387
+ /checkApiKey/i,
388
+ /validateApiKey/i,
389
+ /requireRole/i,
390
+ /hasRole/i,
391
+ /isAdmin/i,
392
+ ]
393
+
394
+ // Search bidirectionally
395
+ const start = Math.max(0, lineIndex - windowSize)
396
+ const end = Math.min(lines.length, lineIndex + windowSize)
397
+
398
+ for (let i = start; i < end; i++) {
399
+ if (authPatterns.some(pattern => pattern.test(lines[i]))) {
400
+ return true
401
+ }
402
+ }
403
+
404
+ return false
405
+ }
406
+
407
+ // ============================================================================
408
+ // BYOK (Bring Your Own Key) Context Detection
409
+ // ============================================================================
410
+
411
+ /**
412
+ * Check if this appears to be a BYOK (user-provided key) context
413
+ * BYOK is a feature, not a vulnerability, unless improperly handled
414
+ */
415
+ export function isBYOKContext(lineContent: string, filePath: string): boolean {
416
+ // Common BYOK patterns
417
+ const byokPatterns = [
418
+ /user.*api.*key/i,
419
+ /customer.*key/i,
420
+ /your.*api.*key/i,
421
+ /provide.*key/i,
422
+ /enter.*key/i,
423
+ /input.*key/i,
424
+ /form.*key/i,
425
+ /settings.*key/i,
426
+ /config.*key.*user/i,
427
+ /BYOK/i,
428
+ /bring.*your.*own/i,
429
+ ]
430
+
431
+ // Form/input contexts
432
+ const inputPatterns = [
433
+ /input.*type/i,
434
+ /onChange/i,
435
+ /onSubmit/i,
436
+ /handleSubmit/i,
437
+ /useState.*key/i,
438
+ /form.*data/i,
439
+ ]
440
+
441
+ // Settings/config UI patterns
442
+ const settingsPatterns = [
443
+ /\/settings\//i,
444
+ /\/config\//i,
445
+ /\/preferences\//i,
446
+ /\/profile\//i,
447
+ ]
448
+
449
+ // Check line content
450
+ if (byokPatterns.some(p => p.test(lineContent)) ||
451
+ inputPatterns.some(p => p.test(lineContent))) {
452
+ return true
453
+ }
454
+
455
+ // Check file path
456
+ if (settingsPatterns.some(p => p.test(filePath))) {
457
+ // In settings files, look for user input context
458
+ if (inputPatterns.some(p => p.test(lineContent))) {
459
+ return true
460
+ }
461
+ }
462
+
463
+ return false
464
+ }
465
+
466
+ /**
467
+ * Check if key is being stored/handled properly (not exposed)
468
+ */
469
+ export function isKeyProperlyHandled(lineContent: string, lines: string[], lineIndex: number): boolean {
470
+ // Proper handling patterns (encryption, secure storage, etc.)
471
+ const properHandlingPatterns = [
472
+ /encrypt/i,
473
+ /hash/i,
474
+ /secure.*storage/i,
475
+ /keychain/i,
476
+ /vault/i,
477
+ /secretsManager/i,
478
+ /kms/i,
479
+ /\.env/i,
480
+ ]
481
+
482
+ // Check current line
483
+ if (properHandlingPatterns.some(p => p.test(lineContent))) {
484
+ return true
485
+ }
486
+
487
+ // Check nearby lines (5 lines before and after)
488
+ const start = Math.max(0, lineIndex - 5)
489
+ const end = Math.min(lines.length, lineIndex + 5)
490
+
491
+ for (let i = start; i < end; i++) {
492
+ if (properHandlingPatterns.some(p => p.test(lines[i]))) {
493
+ return true
494
+ }
495
+ }
496
+
497
+ return false
498
+ }
499
+
500
+ // ============================================================================
501
+ // Service Role Key Context
502
+ // ============================================================================
503
+
504
+ /**
505
+ * Check if this is a service role key usage that's acceptable
506
+ * Server-only + env var = acceptable
507
+ * Client exposure = critical
508
+ */
509
+ export function getServiceRoleKeyContext(
510
+ lineContent: string,
511
+ filePath: string
512
+ ): 'safe_server' | 'needs_review' | 'client_exposure' {
513
+ const isServer = isServerOnlyFile(filePath)
514
+ const usesEnvVar = isEnvVarReference(lineContent)
515
+ const isClientFile = isClientBundledFile(filePath)
516
+ const isNextPublic = isNextPublicEnvVar(lineContent)
517
+
518
+ // NEXT_PUBLIC_ service role key = always critical (client exposure)
519
+ if (isNextPublic) {
520
+ return 'client_exposure'
521
+ }
522
+
523
+ // Server-only file using env var = safe
524
+ if (isServer && usesEnvVar) {
525
+ return 'safe_server'
526
+ }
527
+
528
+ // Client-bundled file = exposure risk
529
+ if (isClientFile) {
530
+ return 'client_exposure'
531
+ }
532
+
533
+ // Hardcoded or ambiguous = needs review
534
+ return 'needs_review'
535
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Diff Detection Utility
3
+ * Compares current repository tree against previous scan data to detect changed files
4
+ * Used for incremental scanning (Story D)
5
+ */
6
+
7
+ export interface TreeFileInfo {
8
+ path: string
9
+ sha: string
10
+ }
11
+
12
+ export interface DiffResult {
13
+ /** Files that are new (not in previous scan) */
14
+ added: string[]
15
+ /** Files whose SHA changed (content modified) */
16
+ modified: string[]
17
+ /** Files that were deleted (in previous scan but not in current) */
18
+ deleted: string[]
19
+ /** Files that haven't changed */
20
+ unchanged: string[]
21
+ /** Total count of changed files (added + modified) */
22
+ changedCount: number
23
+ /** Whether this is a significant change warranting incremental scan */
24
+ shouldUseIncremental: boolean
25
+ }
26
+
27
+ /**
28
+ * Maximum number of changed files before falling back to full scan
29
+ * Beyond this threshold, incremental mode provides diminishing returns
30
+ */
31
+ const MAX_CHANGED_FILES_FOR_INCREMENTAL = 50
32
+
33
+ /**
34
+ * Detect which files have changed between the current tree and previous scan
35
+ *
36
+ * @param currentTree - Array of files from current GitHub tree (path + sha)
37
+ * @param previousFileShas - Map of {file_path: sha} from previous scan
38
+ * @returns DiffResult with categorized file changes
39
+ */
40
+ export function detectChangedFiles(
41
+ currentTree: TreeFileInfo[],
42
+ previousFileShas: Record<string, string> | null | undefined
43
+ ): DiffResult {
44
+ // If no previous data, everything is "new"
45
+ if (!previousFileShas || Object.keys(previousFileShas).length === 0) {
46
+ return {
47
+ added: currentTree.map(f => f.path),
48
+ modified: [],
49
+ deleted: [],
50
+ unchanged: [],
51
+ changedCount: currentTree.length,
52
+ shouldUseIncremental: false, // No baseline = full scan
53
+ }
54
+ }
55
+
56
+ const added: string[] = []
57
+ const modified: string[] = []
58
+ const unchanged: string[] = []
59
+ const currentPaths = new Set<string>()
60
+
61
+ // Compare current files against previous
62
+ for (const file of currentTree) {
63
+ currentPaths.add(file.path)
64
+ const previousSha = previousFileShas[file.path]
65
+
66
+ if (!previousSha) {
67
+ // File didn't exist before
68
+ added.push(file.path)
69
+ } else if (previousSha !== file.sha) {
70
+ // File content changed (SHA differs)
71
+ modified.push(file.path)
72
+ } else {
73
+ // Same SHA = unchanged
74
+ unchanged.push(file.path)
75
+ }
76
+ }
77
+
78
+ // Find deleted files (in previous but not in current)
79
+ const deleted: string[] = []
80
+ for (const path of Object.keys(previousFileShas)) {
81
+ if (!currentPaths.has(path)) {
82
+ deleted.push(path)
83
+ }
84
+ }
85
+
86
+ const changedCount = added.length + modified.length
87
+ const shouldUseIncremental = changedCount > 0 && changedCount <= MAX_CHANGED_FILES_FOR_INCREMENTAL
88
+
89
+ return {
90
+ added,
91
+ modified,
92
+ deleted,
93
+ unchanged,
94
+ changedCount,
95
+ shouldUseIncremental,
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Build a file SHA map from a GitHub tree response
101
+ * Filters to only include blob (file) entries
102
+ *
103
+ * @param tree - GitHub tree items with path and sha
104
+ * @returns Map of {file_path: sha}
105
+ */
106
+ export function buildFileShaMap(
107
+ tree: Array<{ path: string; sha: string; type: 'blob' | 'tree' }>
108
+ ): Record<string, string> {
109
+ const map: Record<string, string> = {}
110
+
111
+ for (const item of tree) {
112
+ // Only include files (blobs), not directories (trees)
113
+ if (item.type === 'blob') {
114
+ map[item.path] = item.sha
115
+ }
116
+ }
117
+
118
+ return map
119
+ }
120
+
121
+ /**
122
+ * Check if two tree SHAs are different
123
+ * Quick check before doing detailed file comparison
124
+ *
125
+ * @param currentTreeSha - SHA of current tree
126
+ * @param previousTreeSha - SHA from previous scan
127
+ * @returns true if trees are different (need detailed comparison)
128
+ */
129
+ export function hasTreeChanged(
130
+ currentTreeSha: string,
131
+ previousTreeSha: string | null | undefined
132
+ ): boolean {
133
+ if (!previousTreeSha) return true
134
+ return currentTreeSha !== previousTreeSha
135
+ }