@oculum/scanner 1.0.9 → 1.0.11

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 (365) hide show
  1. package/dist/baseline/diff.d.ts +32 -0
  2. package/dist/baseline/diff.d.ts.map +1 -0
  3. package/dist/baseline/diff.js +119 -0
  4. package/dist/baseline/diff.js.map +1 -0
  5. package/dist/baseline/index.d.ts +9 -0
  6. package/dist/baseline/index.d.ts.map +1 -0
  7. package/dist/baseline/index.js +19 -0
  8. package/dist/baseline/index.js.map +1 -0
  9. package/dist/baseline/manager.d.ts +67 -0
  10. package/dist/baseline/manager.d.ts.map +1 -0
  11. package/dist/baseline/manager.js +180 -0
  12. package/dist/baseline/manager.js.map +1 -0
  13. package/dist/baseline/types.d.ts +91 -0
  14. package/dist/baseline/types.d.ts.map +1 -0
  15. package/dist/baseline/types.js +12 -0
  16. package/dist/baseline/types.js.map +1 -0
  17. package/dist/formatters/cli-terminal.d.ts +38 -0
  18. package/dist/formatters/cli-terminal.d.ts.map +1 -1
  19. package/dist/formatters/cli-terminal.js +365 -42
  20. package/dist/formatters/cli-terminal.js.map +1 -1
  21. package/dist/formatters/github-comment.d.ts +1 -1
  22. package/dist/formatters/github-comment.d.ts.map +1 -1
  23. package/dist/formatters/github-comment.js +75 -11
  24. package/dist/formatters/github-comment.js.map +1 -1
  25. package/dist/formatters/index.d.ts +1 -1
  26. package/dist/formatters/index.d.ts.map +1 -1
  27. package/dist/formatters/index.js +4 -1
  28. package/dist/formatters/index.js.map +1 -1
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +155 -16
  32. package/dist/index.js.map +1 -1
  33. package/dist/layer1/config-audit.d.ts.map +1 -1
  34. package/dist/layer1/config-audit.js +20 -3
  35. package/dist/layer1/config-audit.js.map +1 -1
  36. package/dist/layer1/config-mcp-audit.d.ts +20 -0
  37. package/dist/layer1/config-mcp-audit.d.ts.map +1 -0
  38. package/dist/layer1/config-mcp-audit.js +239 -0
  39. package/dist/layer1/config-mcp-audit.js.map +1 -0
  40. package/dist/layer1/index.d.ts +1 -0
  41. package/dist/layer1/index.d.ts.map +1 -1
  42. package/dist/layer1/index.js +9 -1
  43. package/dist/layer1/index.js.map +1 -1
  44. package/dist/layer2/ai-agent-tools.d.ts.map +1 -1
  45. package/dist/layer2/ai-agent-tools.js +303 -0
  46. package/dist/layer2/ai-agent-tools.js.map +1 -1
  47. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -1
  48. package/dist/layer2/ai-endpoint-protection.js +17 -3
  49. package/dist/layer2/ai-endpoint-protection.js.map +1 -1
  50. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -1
  51. package/dist/layer2/ai-execution-sinks.js +462 -12
  52. package/dist/layer2/ai-execution-sinks.js.map +1 -1
  53. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -1
  54. package/dist/layer2/ai-fingerprinting.js +3 -0
  55. package/dist/layer2/ai-fingerprinting.js.map +1 -1
  56. package/dist/layer2/ai-mcp-security.d.ts +17 -0
  57. package/dist/layer2/ai-mcp-security.d.ts.map +1 -0
  58. package/dist/layer2/ai-mcp-security.js +679 -0
  59. package/dist/layer2/ai-mcp-security.js.map +1 -0
  60. package/dist/layer2/ai-package-hallucination.d.ts +19 -0
  61. package/dist/layer2/ai-package-hallucination.d.ts.map +1 -0
  62. package/dist/layer2/ai-package-hallucination.js +696 -0
  63. package/dist/layer2/ai-package-hallucination.js.map +1 -0
  64. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -1
  65. package/dist/layer2/ai-prompt-hygiene.js +495 -9
  66. package/dist/layer2/ai-prompt-hygiene.js.map +1 -1
  67. package/dist/layer2/ai-rag-safety.d.ts.map +1 -1
  68. package/dist/layer2/ai-rag-safety.js +372 -1
  69. package/dist/layer2/ai-rag-safety.js.map +1 -1
  70. package/dist/layer2/auth-antipatterns.d.ts.map +1 -1
  71. package/dist/layer2/auth-antipatterns.js +4 -0
  72. package/dist/layer2/auth-antipatterns.js.map +1 -1
  73. package/dist/layer2/byok-patterns.d.ts.map +1 -1
  74. package/dist/layer2/byok-patterns.js +3 -0
  75. package/dist/layer2/byok-patterns.js.map +1 -1
  76. package/dist/layer2/dangerous-functions/child-process.d.ts +16 -0
  77. package/dist/layer2/dangerous-functions/child-process.d.ts.map +1 -0
  78. package/dist/layer2/dangerous-functions/child-process.js +74 -0
  79. package/dist/layer2/dangerous-functions/child-process.js.map +1 -0
  80. package/dist/layer2/dangerous-functions/dom-xss.d.ts +29 -0
  81. package/dist/layer2/dangerous-functions/dom-xss.d.ts.map +1 -0
  82. package/dist/layer2/dangerous-functions/dom-xss.js +179 -0
  83. package/dist/layer2/dangerous-functions/dom-xss.js.map +1 -0
  84. package/dist/layer2/dangerous-functions/index.d.ts +13 -0
  85. package/dist/layer2/dangerous-functions/index.d.ts.map +1 -0
  86. package/dist/layer2/dangerous-functions/index.js +621 -0
  87. package/dist/layer2/dangerous-functions/index.js.map +1 -0
  88. package/dist/layer2/dangerous-functions/json-parse.d.ts +31 -0
  89. package/dist/layer2/dangerous-functions/json-parse.d.ts.map +1 -0
  90. package/dist/layer2/dangerous-functions/json-parse.js +319 -0
  91. package/dist/layer2/dangerous-functions/json-parse.js.map +1 -0
  92. package/dist/layer2/dangerous-functions/math-random.d.ts +61 -0
  93. package/dist/layer2/dangerous-functions/math-random.d.ts.map +1 -0
  94. package/dist/layer2/dangerous-functions/math-random.js +459 -0
  95. package/dist/layer2/dangerous-functions/math-random.js.map +1 -0
  96. package/dist/layer2/dangerous-functions/patterns.d.ts +21 -0
  97. package/dist/layer2/dangerous-functions/patterns.d.ts.map +1 -0
  98. package/dist/layer2/dangerous-functions/patterns.js +161 -0
  99. package/dist/layer2/dangerous-functions/patterns.js.map +1 -0
  100. package/dist/layer2/dangerous-functions/request-validation.d.ts +13 -0
  101. package/dist/layer2/dangerous-functions/request-validation.d.ts.map +1 -0
  102. package/dist/layer2/dangerous-functions/request-validation.js +119 -0
  103. package/dist/layer2/dangerous-functions/request-validation.js.map +1 -0
  104. package/dist/layer2/dangerous-functions/utils/control-flow.d.ts +23 -0
  105. package/dist/layer2/dangerous-functions/utils/control-flow.d.ts.map +1 -0
  106. package/dist/layer2/dangerous-functions/utils/control-flow.js +149 -0
  107. package/dist/layer2/dangerous-functions/utils/control-flow.js.map +1 -0
  108. package/dist/layer2/dangerous-functions/utils/helpers.d.ts +31 -0
  109. package/dist/layer2/dangerous-functions/utils/helpers.d.ts.map +1 -0
  110. package/dist/layer2/dangerous-functions/utils/helpers.js +124 -0
  111. package/dist/layer2/dangerous-functions/utils/helpers.js.map +1 -0
  112. package/dist/layer2/dangerous-functions/utils/index.d.ts +9 -0
  113. package/dist/layer2/dangerous-functions/utils/index.d.ts.map +1 -0
  114. package/dist/layer2/dangerous-functions/utils/index.js +23 -0
  115. package/dist/layer2/dangerous-functions/utils/index.js.map +1 -0
  116. package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts +22 -0
  117. package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts.map +1 -0
  118. package/dist/layer2/dangerous-functions/utils/schema-validation.js +89 -0
  119. package/dist/layer2/dangerous-functions/utils/schema-validation.js.map +1 -0
  120. package/dist/layer2/data-exposure.d.ts.map +1 -1
  121. package/dist/layer2/data-exposure.js +3 -0
  122. package/dist/layer2/data-exposure.js.map +1 -1
  123. package/dist/layer2/framework-checks.d.ts.map +1 -1
  124. package/dist/layer2/framework-checks.js +3 -0
  125. package/dist/layer2/framework-checks.js.map +1 -1
  126. package/dist/layer2/index.d.ts +3 -0
  127. package/dist/layer2/index.d.ts.map +1 -1
  128. package/dist/layer2/index.js +61 -2
  129. package/dist/layer2/index.js.map +1 -1
  130. package/dist/layer2/logic-gates.d.ts.map +1 -1
  131. package/dist/layer2/logic-gates.js +4 -0
  132. package/dist/layer2/logic-gates.js.map +1 -1
  133. package/dist/layer2/model-supply-chain.d.ts +20 -0
  134. package/dist/layer2/model-supply-chain.d.ts.map +1 -0
  135. package/dist/layer2/model-supply-chain.js +376 -0
  136. package/dist/layer2/model-supply-chain.js.map +1 -0
  137. package/dist/layer2/risky-imports.d.ts.map +1 -1
  138. package/dist/layer2/risky-imports.js +4 -0
  139. package/dist/layer2/risky-imports.js.map +1 -1
  140. package/dist/layer2/variables.d.ts.map +1 -1
  141. package/dist/layer2/variables.js +4 -0
  142. package/dist/layer2/variables.js.map +1 -1
  143. package/dist/layer3/anthropic/auto-dismiss.d.ts +24 -0
  144. package/dist/layer3/anthropic/auto-dismiss.d.ts.map +1 -0
  145. package/dist/layer3/anthropic/auto-dismiss.js +188 -0
  146. package/dist/layer3/anthropic/auto-dismiss.js.map +1 -0
  147. package/dist/layer3/anthropic/clients.d.ts +44 -0
  148. package/dist/layer3/anthropic/clients.d.ts.map +1 -0
  149. package/dist/layer3/anthropic/clients.js +81 -0
  150. package/dist/layer3/anthropic/clients.js.map +1 -0
  151. package/dist/layer3/anthropic/index.d.ts +41 -0
  152. package/dist/layer3/anthropic/index.d.ts.map +1 -0
  153. package/dist/layer3/anthropic/index.js +141 -0
  154. package/dist/layer3/anthropic/index.js.map +1 -0
  155. package/dist/layer3/anthropic/prompts/index.d.ts +8 -0
  156. package/dist/layer3/anthropic/prompts/index.d.ts.map +1 -0
  157. package/dist/layer3/anthropic/prompts/index.js +14 -0
  158. package/dist/layer3/anthropic/prompts/index.js.map +1 -0
  159. package/dist/layer3/anthropic/prompts/semantic-analysis.d.ts +15 -0
  160. package/dist/layer3/anthropic/prompts/semantic-analysis.d.ts.map +1 -0
  161. package/dist/layer3/anthropic/prompts/semantic-analysis.js +169 -0
  162. package/dist/layer3/anthropic/prompts/semantic-analysis.js.map +1 -0
  163. package/dist/layer3/anthropic/prompts/validation.d.ts +12 -0
  164. package/dist/layer3/anthropic/prompts/validation.d.ts.map +1 -0
  165. package/dist/layer3/anthropic/prompts/validation.js +421 -0
  166. package/dist/layer3/anthropic/prompts/validation.js.map +1 -0
  167. package/dist/layer3/anthropic/providers/anthropic.d.ts +21 -0
  168. package/dist/layer3/anthropic/providers/anthropic.d.ts.map +1 -0
  169. package/dist/layer3/anthropic/providers/anthropic.js +266 -0
  170. package/dist/layer3/anthropic/providers/anthropic.js.map +1 -0
  171. package/dist/layer3/anthropic/providers/index.d.ts +8 -0
  172. package/dist/layer3/anthropic/providers/index.d.ts.map +1 -0
  173. package/dist/layer3/anthropic/providers/index.js +15 -0
  174. package/dist/layer3/anthropic/providers/index.js.map +1 -0
  175. package/dist/layer3/anthropic/providers/openai.d.ts +18 -0
  176. package/dist/layer3/anthropic/providers/openai.d.ts.map +1 -0
  177. package/dist/layer3/anthropic/providers/openai.js +340 -0
  178. package/dist/layer3/anthropic/providers/openai.js.map +1 -0
  179. package/dist/layer3/anthropic/request-builder.d.ts +20 -0
  180. package/dist/layer3/anthropic/request-builder.d.ts.map +1 -0
  181. package/dist/layer3/anthropic/request-builder.js +134 -0
  182. package/dist/layer3/anthropic/request-builder.js.map +1 -0
  183. package/dist/layer3/anthropic/types.d.ts +88 -0
  184. package/dist/layer3/anthropic/types.d.ts.map +1 -0
  185. package/dist/layer3/anthropic/types.js +38 -0
  186. package/dist/layer3/anthropic/types.js.map +1 -0
  187. package/dist/layer3/anthropic/utils/index.d.ts +9 -0
  188. package/dist/layer3/anthropic/utils/index.d.ts.map +1 -0
  189. package/dist/layer3/anthropic/utils/index.js +24 -0
  190. package/dist/layer3/anthropic/utils/index.js.map +1 -0
  191. package/dist/layer3/anthropic/utils/path-helpers.d.ts +21 -0
  192. package/dist/layer3/anthropic/utils/path-helpers.d.ts.map +1 -0
  193. package/dist/layer3/anthropic/utils/path-helpers.js +69 -0
  194. package/dist/layer3/anthropic/utils/path-helpers.js.map +1 -0
  195. package/dist/layer3/anthropic/utils/response-parser.d.ts +40 -0
  196. package/dist/layer3/anthropic/utils/response-parser.d.ts.map +1 -0
  197. package/dist/layer3/anthropic/utils/response-parser.js +285 -0
  198. package/dist/layer3/anthropic/utils/response-parser.js.map +1 -0
  199. package/dist/layer3/anthropic/utils/retry.d.ts +15 -0
  200. package/dist/layer3/anthropic/utils/retry.d.ts.map +1 -0
  201. package/dist/layer3/anthropic/utils/retry.js +62 -0
  202. package/dist/layer3/anthropic/utils/retry.js.map +1 -0
  203. package/dist/layer3/index.d.ts +1 -0
  204. package/dist/layer3/index.d.ts.map +1 -1
  205. package/dist/layer3/index.js +16 -6
  206. package/dist/layer3/index.js.map +1 -1
  207. package/dist/layer3/osv-check.d.ts +75 -0
  208. package/dist/layer3/osv-check.d.ts.map +1 -0
  209. package/dist/layer3/osv-check.js +308 -0
  210. package/dist/layer3/osv-check.js.map +1 -0
  211. package/dist/rules/framework-fixes.d.ts +48 -0
  212. package/dist/rules/framework-fixes.d.ts.map +1 -0
  213. package/dist/rules/framework-fixes.js +439 -0
  214. package/dist/rules/framework-fixes.js.map +1 -0
  215. package/dist/rules/index.d.ts +8 -0
  216. package/dist/rules/index.d.ts.map +1 -0
  217. package/dist/rules/index.js +18 -0
  218. package/dist/rules/index.js.map +1 -0
  219. package/dist/rules/metadata.d.ts +43 -0
  220. package/dist/rules/metadata.d.ts.map +1 -0
  221. package/dist/rules/metadata.js +734 -0
  222. package/dist/rules/metadata.js.map +1 -0
  223. package/dist/suppression/config-loader.d.ts +74 -0
  224. package/dist/suppression/config-loader.d.ts.map +1 -0
  225. package/dist/suppression/config-loader.js +424 -0
  226. package/dist/suppression/config-loader.js.map +1 -0
  227. package/dist/suppression/hash.d.ts +48 -0
  228. package/dist/suppression/hash.d.ts.map +1 -0
  229. package/dist/suppression/hash.js +88 -0
  230. package/dist/suppression/hash.js.map +1 -0
  231. package/dist/suppression/index.d.ts +11 -0
  232. package/dist/suppression/index.d.ts.map +1 -0
  233. package/dist/suppression/index.js +39 -0
  234. package/dist/suppression/index.js.map +1 -0
  235. package/dist/suppression/inline-parser.d.ts +39 -0
  236. package/dist/suppression/inline-parser.d.ts.map +1 -0
  237. package/dist/suppression/inline-parser.js +218 -0
  238. package/dist/suppression/inline-parser.js.map +1 -0
  239. package/dist/suppression/manager.d.ts +94 -0
  240. package/dist/suppression/manager.d.ts.map +1 -0
  241. package/dist/suppression/manager.js +292 -0
  242. package/dist/suppression/manager.js.map +1 -0
  243. package/dist/suppression/types.d.ts +151 -0
  244. package/dist/suppression/types.d.ts.map +1 -0
  245. package/dist/suppression/types.js +28 -0
  246. package/dist/suppression/types.js.map +1 -0
  247. package/dist/tiers.d.ts +1 -1
  248. package/dist/tiers.d.ts.map +1 -1
  249. package/dist/tiers.js +27 -0
  250. package/dist/tiers.js.map +1 -1
  251. package/dist/types.d.ts +62 -1
  252. package/dist/types.d.ts.map +1 -1
  253. package/dist/types.js.map +1 -1
  254. package/dist/utils/context-helpers.d.ts +4 -0
  255. package/dist/utils/context-helpers.d.ts.map +1 -1
  256. package/dist/utils/context-helpers.js +13 -9
  257. package/dist/utils/context-helpers.js.map +1 -1
  258. package/package.json +4 -2
  259. package/src/__tests__/benchmark/fixtures/layer1/mcp-config-audit.json +31 -0
  260. package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +1489 -82
  261. package/src/__tests__/benchmark/fixtures/layer2/ai-mcp-security.ts +495 -0
  262. package/src/__tests__/benchmark/fixtures/layer2/ai-package-hallucination.ts +255 -0
  263. package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +300 -1
  264. package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +139 -0
  265. package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +7 -0
  266. package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +63 -0
  267. package/src/__tests__/benchmark/fixtures/layer2/excessive-agency.ts +221 -0
  268. package/src/__tests__/benchmark/fixtures/layer2/index.ts +18 -0
  269. package/src/__tests__/benchmark/fixtures/layer2/model-supply-chain.ts +204 -0
  270. package/src/__tests__/benchmark/fixtures/layer2/phase1-enhancements.ts +157 -0
  271. package/src/__tests__/snapshots/__snapshots__/anthropic-validation-refactor.test.ts.snap +758 -0
  272. package/src/__tests__/snapshots/__snapshots__/dangerous-functions-refactor.test.ts.snap +503 -0
  273. package/src/__tests__/snapshots/anthropic-validation-refactor.test.ts +321 -0
  274. package/src/__tests__/snapshots/dangerous-functions-refactor.test.ts +439 -0
  275. package/src/baseline/__tests__/diff.test.ts +261 -0
  276. package/src/baseline/__tests__/manager.test.ts +225 -0
  277. package/src/baseline/diff.ts +135 -0
  278. package/src/baseline/index.ts +29 -0
  279. package/src/baseline/manager.ts +230 -0
  280. package/src/baseline/types.ts +97 -0
  281. package/src/formatters/cli-terminal.ts +444 -41
  282. package/src/formatters/github-comment.ts +79 -11
  283. package/src/formatters/index.ts +4 -0
  284. package/src/index.ts +197 -14
  285. package/src/layer1/config-audit.ts +24 -3
  286. package/src/layer1/config-mcp-audit.ts +276 -0
  287. package/src/layer1/index.ts +16 -6
  288. package/src/layer2/ai-agent-tools.ts +336 -0
  289. package/src/layer2/ai-endpoint-protection.ts +16 -3
  290. package/src/layer2/ai-execution-sinks.ts +516 -12
  291. package/src/layer2/ai-fingerprinting.ts +5 -1
  292. package/src/layer2/ai-mcp-security.ts +730 -0
  293. package/src/layer2/ai-package-hallucination.ts +791 -0
  294. package/src/layer2/ai-prompt-hygiene.ts +547 -9
  295. package/src/layer2/ai-rag-safety.ts +382 -3
  296. package/src/layer2/auth-antipatterns.ts +5 -0
  297. package/src/layer2/byok-patterns.ts +5 -1
  298. package/src/layer2/dangerous-functions/child-process.ts +98 -0
  299. package/src/layer2/dangerous-functions/dom-xss.ts +220 -0
  300. package/src/layer2/dangerous-functions/index.ts +949 -0
  301. package/src/layer2/dangerous-functions/json-parse.ts +385 -0
  302. package/src/layer2/dangerous-functions/math-random.ts +537 -0
  303. package/src/layer2/dangerous-functions/patterns.ts +174 -0
  304. package/src/layer2/dangerous-functions/request-validation.ts +145 -0
  305. package/src/layer2/dangerous-functions/utils/control-flow.ts +162 -0
  306. package/src/layer2/dangerous-functions/utils/helpers.ts +170 -0
  307. package/src/layer2/dangerous-functions/utils/index.ts +25 -0
  308. package/src/layer2/dangerous-functions/utils/schema-validation.ts +91 -0
  309. package/src/layer2/data-exposure.ts +5 -1
  310. package/src/layer2/framework-checks.ts +5 -0
  311. package/src/layer2/index.ts +63 -1
  312. package/src/layer2/logic-gates.ts +5 -0
  313. package/src/layer2/model-supply-chain.ts +456 -0
  314. package/src/layer2/risky-imports.ts +5 -0
  315. package/src/layer2/variables.ts +5 -0
  316. package/src/layer3/__tests__/osv-check.test.ts +384 -0
  317. package/src/layer3/anthropic/auto-dismiss.ts +212 -0
  318. package/src/layer3/anthropic/clients.ts +84 -0
  319. package/src/layer3/anthropic/index.ts +170 -0
  320. package/src/layer3/anthropic/prompts/index.ts +14 -0
  321. package/src/layer3/anthropic/prompts/semantic-analysis.ts +173 -0
  322. package/src/layer3/anthropic/prompts/validation.ts +419 -0
  323. package/src/layer3/anthropic/providers/anthropic.ts +310 -0
  324. package/src/layer3/anthropic/providers/index.ts +8 -0
  325. package/src/layer3/anthropic/providers/openai.ts +384 -0
  326. package/src/layer3/anthropic/request-builder.ts +150 -0
  327. package/src/layer3/anthropic/types.ts +148 -0
  328. package/src/layer3/anthropic/utils/index.ts +26 -0
  329. package/src/layer3/anthropic/utils/path-helpers.ts +68 -0
  330. package/src/layer3/anthropic/utils/response-parser.ts +322 -0
  331. package/src/layer3/anthropic/utils/retry.ts +75 -0
  332. package/src/layer3/index.ts +18 -5
  333. package/src/layer3/osv-check.ts +420 -0
  334. package/src/rules/__tests__/framework-fixes.test.ts +689 -0
  335. package/src/rules/__tests__/metadata.test.ts +218 -0
  336. package/src/rules/framework-fixes.ts +470 -0
  337. package/src/rules/index.ts +21 -0
  338. package/src/rules/metadata.ts +831 -0
  339. package/src/suppression/__tests__/config-loader.test.ts +382 -0
  340. package/src/suppression/__tests__/hash.test.ts +166 -0
  341. package/src/suppression/__tests__/inline-parser.test.ts +212 -0
  342. package/src/suppression/__tests__/manager.test.ts +415 -0
  343. package/src/suppression/config-loader.ts +462 -0
  344. package/src/suppression/hash.ts +95 -0
  345. package/src/suppression/index.ts +51 -0
  346. package/src/suppression/inline-parser.ts +273 -0
  347. package/src/suppression/manager.ts +379 -0
  348. package/src/suppression/types.ts +174 -0
  349. package/src/tiers.ts +36 -0
  350. package/src/types.ts +90 -0
  351. package/src/utils/context-helpers.ts +13 -9
  352. package/dist/layer2/dangerous-functions.d.ts +0 -7
  353. package/dist/layer2/dangerous-functions.d.ts.map +0 -1
  354. package/dist/layer2/dangerous-functions.js +0 -1701
  355. package/dist/layer2/dangerous-functions.js.map +0 -1
  356. package/dist/layer3/anthropic.d.ts +0 -87
  357. package/dist/layer3/anthropic.d.ts.map +0 -1
  358. package/dist/layer3/anthropic.js +0 -1948
  359. package/dist/layer3/anthropic.js.map +0 -1
  360. package/dist/layer3/openai.d.ts +0 -25
  361. package/dist/layer3/openai.d.ts.map +0 -1
  362. package/dist/layer3/openai.js +0 -238
  363. package/dist/layer3/openai.js.map +0 -1
  364. package/src/layer2/dangerous-functions.ts +0 -1940
  365. package/src/layer3/anthropic.ts +0 -2257
@@ -35,7 +35,29 @@ const CATEGORY_DOCS: Partial<Record<VulnerabilityCategory, string>> = {
35
35
  }
36
36
 
37
37
  /**
38
- * Format a single finding as a markdown list item
38
+ * Helper to determine language from file path
39
+ */
40
+ function getLanguageFromPath(filePath: string): string {
41
+ const ext = filePath.split('.').pop()?.toLowerCase() || ''
42
+ const langMap: Record<string, string> = {
43
+ ts: 'typescript',
44
+ tsx: 'typescript',
45
+ js: 'javascript',
46
+ jsx: 'javascript',
47
+ py: 'python',
48
+ go: 'go',
49
+ java: 'java',
50
+ rb: 'ruby',
51
+ php: 'php',
52
+ yaml: 'yaml',
53
+ yml: 'yaml',
54
+ json: 'json',
55
+ }
56
+ return langMap[ext] || ''
57
+ }
58
+
59
+ /**
60
+ * Format a single finding as a markdown section with actionable info (PRO-82)
39
61
  */
40
62
  function formatFinding(finding: Vulnerability, options: { showFile?: boolean; showDocs?: boolean } = {}): string {
41
63
  const { showFile = true, showDocs = true } = options
@@ -44,18 +66,43 @@ function formatFinding(finding: Vulnerability, options: { showFile?: boolean; sh
44
66
  ? `\`${finding.filePath}:${finding.lineNumber}\``
45
67
  : `Line ${finding.lineNumber}`
46
68
 
47
- let md = `- ${badge} **${finding.title}**\n`
48
- md += ` - 📍 ${location}\n`
49
- md += ` - ${finding.description}\n`
69
+ let md = `#### ${badge} ${finding.title}\n\n`
70
+ md += `📍 ${location}\n\n`
71
+
72
+ // Impact (why this matters) - shown if available
73
+ if (finding.impact) {
74
+ md += `**Impact:** ${finding.impact}\n\n`
75
+ }
76
+
77
+ // Code snippet in collapsible
78
+ if (finding.lineContent && finding.lineContent.trim()) {
79
+ const language = getLanguageFromPath(finding.filePath)
80
+ md += `<details>\n<summary>View code</summary>\n\n`
81
+ md += `\`\`\`${language}\n${finding.lineContent.trim()}\n\`\`\`\n\n`
82
+ md += `</details>\n\n`
83
+ }
50
84
 
51
- if (finding.suggestedFix) {
52
- md += ` - 💡 **Fix:** ${finding.suggestedFix}\n`
85
+ // Fix steps - shown as numbered list (PRO-82)
86
+ if (finding.fixSteps && finding.fixSteps.length > 0) {
87
+ md += `**Fix:**\n`
88
+ finding.fixSteps.forEach((step, i) => {
89
+ md += `${i + 1}. ${step}\n`
90
+ })
91
+ md += '\n'
92
+ } else if (finding.suggestedFix) {
93
+ // Fallback to legacy field
94
+ md += `💡 **Fix:** ${finding.suggestedFix}\n\n`
53
95
  }
54
96
 
55
- // Add documentation link if available
97
+ // Documentation links
56
98
  const docsUrl = CATEGORY_DOCS[finding.category]
57
- if (showDocs && docsUrl) {
58
- md += ` - 📚 [Learn more](${docsUrl})\n`
99
+ const referenceUrl = finding.references && finding.references.length > 0 ? finding.references[0] : null
100
+
101
+ if (showDocs && (docsUrl || referenceUrl)) {
102
+ const links: string[] = []
103
+ if (docsUrl) links.push(`[Learn more](${docsUrl})`)
104
+ if (referenceUrl && referenceUrl !== docsUrl) links.push(`[OWASP/CWE](${referenceUrl})`)
105
+ md += links.join(' · ') + '\n\n'
59
106
  }
60
107
 
61
108
  return md
@@ -357,7 +404,7 @@ export function formatShortStatus(result: ScanResult): string {
357
404
  }
358
405
 
359
406
  /**
360
- * Format as inline annotation for GitHub check run
407
+ * Format as inline annotation for GitHub check run (PRO-82: actionable output)
361
408
  */
362
409
  export function formatAnnotation(finding: Vulnerability): {
363
410
  path: string
@@ -371,12 +418,33 @@ export function formatAnnotation(finding: Vulnerability): {
371
418
  finding.severity === 'critical' || finding.severity === 'high' ? 'failure' :
372
419
  finding.severity === 'medium' ? 'warning' : 'notice'
373
420
 
421
+ // Build actionable message
422
+ let message = ''
423
+
424
+ // Impact first (why this matters)
425
+ if (finding.impact) {
426
+ message += `Impact: ${finding.impact}\n\n`
427
+ }
428
+
429
+ // Description
430
+ message += finding.description
431
+
432
+ // Fix steps or legacy suggestedFix
433
+ if (finding.fixSteps && finding.fixSteps.length > 0) {
434
+ message += '\n\n💡 Fix:\n'
435
+ finding.fixSteps.forEach((step, i) => {
436
+ message += `${i + 1}. ${step}\n`
437
+ })
438
+ } else if (finding.suggestedFix) {
439
+ message += `\n\n💡 Fix: ${finding.suggestedFix}`
440
+ }
441
+
374
442
  return {
375
443
  path: finding.filePath,
376
444
  start_line: finding.lineNumber,
377
445
  end_line: finding.lineNumber,
378
446
  annotation_level: level,
379
447
  title: `${SEVERITY_BADGE[finding.severity]} ${finding.title}`,
380
- message: finding.description + (finding.suggestedFix ? `\n\n💡 Fix: ${finding.suggestedFix}` : ''),
448
+ message,
381
449
  }
382
450
  }
@@ -44,4 +44,8 @@ export {
44
44
  formatSimpleList,
45
45
  formatJSON,
46
46
  formatSARIF,
47
+ formatCompactSummary,
48
+ getNumberedFindings,
49
+ formatFindingDetail,
50
+ type CompactSummaryOptions,
47
51
  } from './cli-terminal'
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  ScanModeConfig,
15
15
  ScanDepth,
16
16
  CancellationToken,
17
+ SuppressedVulnerabilitySummary,
17
18
  } from './types'
18
19
  import { SCANNABLE_EXTENSIONS, SPECIAL_FILES, MAX_FILE_SIZE, SCAN_MODE_DEFAULTS } from './types'
19
20
  import { runLayer1Scan } from './layer1'
@@ -34,6 +35,14 @@ import {
34
35
  computeTierStats,
35
36
  formatTierStats,
36
37
  } from './tiers'
38
+ // Suppression system
39
+ import { SuppressionManager } from './suppression'
40
+ // Rule metadata for actionable output (PRO-82)
41
+ import { getRuleMetadata } from './rules'
42
+ // Framework-aware fix suggestions (PRO-83)
43
+ import { getFrameworkFix } from './rules/framework-fixes'
44
+ // Project context for framework detection
45
+ import { buildProjectContext, type ProjectContext } from './utils/project-context-builder'
37
46
 
38
47
  // Maximum candidates per file to send to AI validation (cost control)
39
48
  const MAX_VALIDATION_CANDIDATES_PER_FILE = 10
@@ -143,6 +152,10 @@ export interface ScanOptions {
143
152
  quiet?: boolean
144
153
  /** Cancellation token for aborting scans gracefully */
145
154
  cancellationToken?: CancellationToken
155
+ /** Project path for loading suppression config (defaults to cwd) */
156
+ projectPath?: string
157
+ /** Include suppressed findings in output (for --show-suppressed) */
158
+ showSuppressed?: boolean
146
159
  }
147
160
 
148
161
  export interface ScanProgress {
@@ -286,7 +299,11 @@ export async function runScan(
286
299
 
287
300
  checkCancelled()
288
301
 
302
+ // Phase timing tracking
303
+ const phaseTiming: { layer1?: number; layer2?: number; aiValidation?: number; layer3?: number } = {}
304
+
289
305
  // Layer 1: Surface Scan
306
+ const layer1Start = Date.now()
290
307
  reportProgress('layer1', 'Running surface scan (patterns, entropy, config)...')
291
308
  let layer1Result = await runLayer1Scan(files, onProgress, cancellationToken)
292
309
 
@@ -296,11 +313,13 @@ export async function runScan(
296
313
  ...layer1Result,
297
314
  vulnerabilities: aggregateLocalhostFindings(layer1Result.vulnerabilities)
298
315
  }
299
- log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`)
316
+ phaseTiming.layer1 = Date.now() - layer1Start
317
+ log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length} duration=${phaseTiming.layer1}ms`)
300
318
 
301
319
  checkCancelled()
302
320
 
303
321
  // Layer 2: Structural Scan
322
+ const layer2Start = Date.now()
304
323
  reportProgress('layer2', 'Running structural scan (variables, logic gates)...', layer1Result.vulnerabilities.length)
305
324
  const layer2Result = await runLayer2Scan(files, { middlewareConfig, fileAuthImports }, onProgress, cancellationToken)
306
325
 
@@ -309,7 +328,8 @@ export async function runScan(
309
328
  .filter(([, count]) => count > 0)
310
329
  .map(([name, count]) => `${name}:${count}`)
311
330
  .join(',')
312
- log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b) => a + b, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`)
331
+ phaseTiming.layer2 = Date.now() - layer2Start
332
+ log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b) => a + b, 0)} findings_deduped=${layer2Result.vulnerabilities.length} duration=${phaseTiming.layer2}ms heuristic_breakdown={${heuristicBreakdown}}`)
313
333
 
314
334
  // Combine Layer 1 and Layer 2 findings
315
335
  const layer12Findings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities]
@@ -319,9 +339,19 @@ export async function runScan(
319
339
  const aggregatedFindings = aggregateNoisyFindings(layer12Findings)
320
340
  const aggregatedCount = beforeAggregationCount - aggregatedFindings.length
321
341
 
342
+ // Build project context for framework-aware fixes (PRO-83)
343
+ // This detects frameworks (Next.js, Express), ORMs (Prisma, Drizzle), and frontend libs (React, Vue)
344
+ const projectContext = buildProjectContext(files)
345
+
346
+ // Enrich findings with metadata from rule registry (PRO-82)
347
+ // PRO-83: Uses projectContext for framework-specific fix suggestions
348
+ // This provides default impact, evidence, fixSteps, references for all findings
349
+ // AI validation can override these later with context-aware content
350
+ const enrichedFindings = enrichWithMetadata(aggregatedFindings, projectContext)
351
+
322
352
  // Apply tier-based filtering based on scan depth
323
353
  // This is the key integration point for the detector tier system
324
- const tierFiltered = filterByTierAndDepth(aggregatedFindings, depth)
354
+ const tierFiltered = filterByTierAndDepth(enrichedFindings, depth)
325
355
 
326
356
  // Log tier breakdown
327
357
  log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${formatTierStats(tierFiltered.tierStats)}`)
@@ -339,7 +369,12 @@ export async function runScan(
339
369
  )
340
370
 
341
371
  // Surface findings that don't need validation (excluding those that do)
342
- const noValidationNeeded = tierFiltered.toSurface.filter(v => !additionalValidation.includes(v))
372
+ const noValidationNeededRaw = tierFiltered.toSurface.filter(v => !additionalValidation.includes(v))
373
+
374
+ // Apply auto-dismiss rules to direct-surface findings (mode='surface')
375
+ // Uses 'surface' mode to exclude cost-saving rules like 'info_severity_core_only'
376
+ // This ensures test/scanner/example files are dismissed, but info-severity findings still surface
377
+ const { toValidate: noValidationNeeded, dismissed: surfaceDismissed } = applyAutoDismissRules(noValidationNeededRaw, 'surface')
343
378
 
344
379
  // Combine tier-filtered validation candidates with additional ones
345
380
  const requiresValidation = [...tierFiltered.toValidate, ...additionalValidation]
@@ -347,13 +382,22 @@ export async function runScan(
347
382
  // Apply smart auto-dismiss rules BEFORE AI validation (saves API costs)
348
383
  const { toValidate: afterAutoDismiss, dismissed: autoDismissed } = applyAutoDismissRules(requiresValidation)
349
384
 
350
- // Track auto-dismiss by severity for logging
385
+ // Combine all dismissed findings for logging
386
+ const allDismissed = [...surfaceDismissed, ...autoDismissed]
387
+
388
+ // Track auto-dismiss by severity and category for logging
351
389
  const autoDismissBySeverity: Record<string, number> = { info: 0, low: 0, medium: 0, high: 0, critical: 0 }
352
- for (const d of autoDismissed) {
390
+ const autoDismissByCategory: Record<string, number> = {}
391
+ for (const d of allDismissed) {
353
392
  autoDismissBySeverity[d.finding.severity] = (autoDismissBySeverity[d.finding.severity] || 0) + 1
393
+ autoDismissByCategory[d.finding.category] = (autoDismissByCategory[d.finding.category] || 0) + 1
354
394
  }
355
- if (autoDismissed.length > 0) {
356
- log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`)
395
+ if (allDismissed.length > 0) {
396
+ const categoryBreakdown = Object.entries(autoDismissByCategory)
397
+ .sort(([, a], [, b]) => b - a)
398
+ .map(([cat, count]) => `${cat}:${count}`)
399
+ .join(',')
400
+ log(`[AutoDismiss] repo=${repoInfo.name} total=${allDismissed.length} (surface=${surfaceDismissed.length} validation=${autoDismissed.length}) by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}} by_category={${categoryBreakdown}}`)
357
401
  }
358
402
 
359
403
  // Apply per-file cap to validation candidates (cost control)
@@ -370,6 +414,7 @@ export async function runScan(
370
414
 
371
415
  if (shouldValidate) {
372
416
  checkCancelled()
417
+ const aiValidationStart = Date.now()
373
418
  reportProgress('validating', 'AI validating findings (entropy, secrets, AI patterns)...', cappedValidation.length)
374
419
 
375
420
  // For incremental scans, only validate findings in changed files
@@ -396,8 +441,9 @@ export async function runScan(
396
441
  validatedFindings = validationResult.vulnerabilities
397
442
  const { stats: validationStats } = validationResult
398
443
  capturedValidationStats = validationStats // Capture for return
444
+ phaseTiming.aiValidation = Date.now() - aiValidationStart
399
445
 
400
- log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`)
446
+ log(`[AI Validation] repo=${repoInfo.name} depth=${depth} duration=${phaseTiming.aiValidation}ms candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`)
401
447
  log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`)
402
448
 
403
449
  // Add back findings that weren't validated (not in changed files)
@@ -411,7 +457,11 @@ export async function runScan(
411
457
  validatedFindings.push(...notValidated)
412
458
  }
413
459
  } else if (scanModeConfig.skipAIValidation) {
414
- log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`)
460
+ log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config findings_requiring_validation=${cappedValidation.length}`)
461
+ // In cheap mode, don't surface findings that require AI validation
462
+ // These are low-confidence without validation and would be noise
463
+ // Only surface high-confidence findings that don't need validation
464
+ validatedFindings = []
415
465
  }
416
466
 
417
467
  // Combine validated and non-validated findings
@@ -423,6 +473,7 @@ export async function runScan(
423
473
 
424
474
  if (shouldRunLayer3) {
425
475
  checkCancelled()
476
+ const layer3Start = Date.now()
426
477
  reportProgress('layer3', 'Running AI semantic analysis...', allVulnerabilities.length)
427
478
 
428
479
  // For incremental scans, only analyze changed files
@@ -448,26 +499,64 @@ export async function runScan(
448
499
  },
449
500
  cancellationToken,
450
501
  })
502
+ phaseTiming.layer3 = Date.now() - layer3Start
451
503
  allVulnerabilities.push(...layer3Result.vulnerabilities)
452
- log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`)
504
+ log(`[Layer3] repo=${repoInfo.name} depth=${depth} duration=${phaseTiming.layer3}ms files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`)
453
505
  } else if (scanModeConfig.skipLayer3) {
454
506
  log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`)
455
507
  }
456
508
 
509
+ // Log phase timing summary
510
+ const phaseTimingStr = Object.entries(phaseTiming)
511
+ .filter(([, ms]) => ms !== undefined)
512
+ .map(([phase, ms]) => `${phase}=${ms}ms`)
513
+ .join(' ')
514
+ if (phaseTimingStr) {
515
+ log(`[Scanner] repo=${repoInfo.name} phase_timing: ${phaseTimingStr}`)
516
+ }
517
+
457
518
  // Deduplicate vulnerabilities
458
519
  const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities)
459
520
 
460
521
  // Resolve contradictions (e.g., middleware-protected INFO vs missing-auth CRITICAL on same route)
461
522
  const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig)
462
-
523
+
524
+ // Apply suppressions (inline comments + config file)
525
+ const projectPath = options.projectPath || process.cwd()
526
+ const suppressionManager = new SuppressionManager({ projectPath })
527
+ const suppressionResult = suppressionManager.applySuppressions(resolvedVulnerabilities, files)
528
+
529
+ // Log suppression stats if any were suppressed
530
+ if (suppressionResult.suppressed.length > 0 || suppressionResult.expiredSuppressions > 0) {
531
+ log(`[Suppression] repo=${repoInfo.name} suppressed=${suppressionResult.suppressed.length} (inline=${suppressionResult.stats.inlineSuppressed} config_finding=${suppressionResult.stats.configFindingSuppressed} config_rule=${suppressionResult.stats.configRuleSuppressed}) expired=${suppressionResult.expiredSuppressions}`)
532
+ }
533
+
534
+ // Use the filtered findings (after suppression)
535
+ const afterSuppression = suppressionResult.findings
536
+
463
537
  // Sort by severity
464
- const sortedVulnerabilities = sortBySeverity(resolvedVulnerabilities)
538
+ const sortedVulnerabilities = sortBySeverity(afterSuppression)
465
539
 
466
- // Compute issue-mix counts
540
+ // Compute issue-mix counts (based on unsuppressed findings)
467
541
  const severityCounts = computeSeverityCounts(sortedVulnerabilities)
468
542
  const categoryCounts = computeCategoryCounts(sortedVulnerabilities)
469
543
  const hasBlockingIssues = severityCounts.critical > 0 || severityCounts.high > 0
470
544
 
545
+ // Build suppressed vulnerabilities summary (for --show-suppressed)
546
+ const suppressedVulnerabilities: SuppressedVulnerabilitySummary[] | undefined = options.showSuppressed
547
+ ? suppressionResult.suppressed.map(s => ({
548
+ hash: s.suppression.hash,
549
+ filePath: s.vulnerability.filePath,
550
+ lineNumber: s.vulnerability.lineNumber,
551
+ category: s.vulnerability.category,
552
+ severity: s.vulnerability.severity,
553
+ title: s.vulnerability.title,
554
+ suppressionType: s.suppression.type,
555
+ suppressionReason: s.suppression.reason,
556
+ expires: s.suppression.expires,
557
+ }))
558
+ : undefined
559
+
471
560
  reportProgress('complete', 'Scan complete!', sortedVulnerabilities.length)
472
561
 
473
562
  return {
@@ -483,6 +572,10 @@ export async function runScan(
483
572
  scanDuration: Date.now() - startTime,
484
573
  timestamp: new Date().toISOString(),
485
574
  validationStats: capturedValidationStats,
575
+ suppressionStats: suppressionResult.suppressed.length > 0 || suppressionResult.expiredSuppressions > 0
576
+ ? suppressionResult.stats
577
+ : undefined,
578
+ suppressedVulnerabilities,
486
579
  }
487
580
  } catch (error) {
488
581
  if (cancellationToken?.cancelled) {
@@ -519,6 +612,50 @@ export async function runScan(
519
612
  }
520
613
  }
521
614
 
615
+ /**
616
+ * Enrich findings with metadata from the rule registry (PRO-82)
617
+ * Sets default impact, evidence, fixSteps, and references from registry
618
+ *
619
+ * PRO-83: When projectContext is provided, uses framework-aware fix suggestions
620
+ * that are tailored to the user's detected tech stack (e.g., Prisma-specific
621
+ * SQL injection fixes instead of generic advice).
622
+ *
623
+ * These can be overridden later by AI-generated content
624
+ */
625
+ function enrichWithMetadata(
626
+ findings: Vulnerability[],
627
+ projectContext?: ProjectContext
628
+ ): Vulnerability[] {
629
+ return findings.map(f => {
630
+ const metadata = getRuleMetadata(f.category)
631
+ if (!metadata) return f
632
+
633
+ // PRO-83: Check for framework-specific fix suggestions
634
+ let fixSteps = metadata.fixSteps
635
+ if (projectContext) {
636
+ const frameworkFix = getFrameworkFix(
637
+ f.category,
638
+ projectContext.frameworks,
639
+ projectContext.dataAccess
640
+ )
641
+ if (frameworkFix) {
642
+ fixSteps = frameworkFix.fixSteps
643
+ // Optionally append code example to description if available
644
+ // This makes the fix more actionable
645
+ }
646
+ }
647
+
648
+ return {
649
+ ...f,
650
+ // Set defaults from registry (AI can override later)
651
+ impact: f.impact || metadata.whyItMatters,
652
+ evidence: f.evidence || metadata.evidence,
653
+ fixSteps: f.fixSteps || fixSteps,
654
+ references: f.references || metadata.references,
655
+ }
656
+ })
657
+ }
658
+
522
659
  /**
523
660
  * Aggregate noisy findings in the same file to reduce clutter
524
661
  * Groups repeated findings with same filePath + category + title
@@ -905,3 +1042,49 @@ export { runLayer3Scan } from './layer3'
905
1042
  export { buildProjectContext, type ProjectContext } from './utils/project-context-builder'
906
1043
  export { validateFindingsWithAI, type ValidationStats, type AIValidationResult } from './layer3/anthropic'
907
1044
  export { createCancellationToken } from './types'
1045
+
1046
+ // Suppression system exports
1047
+ export {
1048
+ SuppressionManager,
1049
+ computeFindingHash,
1050
+ loadSuppressionConfig,
1051
+ addFindingSuppression,
1052
+ removeFindingSuppression,
1053
+ addRuleSuppression,
1054
+ listSuppressions,
1055
+ parseInlineSuppressions,
1056
+ generateSuppressionComment,
1057
+ isValidHash,
1058
+ type SuppressionConfig,
1059
+ type FindingSuppression,
1060
+ type RuleSuppression,
1061
+ type SuppressionResult,
1062
+ type SuppressedVulnerability,
1063
+ } from './suppression'
1064
+
1065
+ // Baseline system exports
1066
+ export {
1067
+ BaselineManager,
1068
+ computeDiff,
1069
+ hasNewBlockingIssues,
1070
+ formatDiffSummary,
1071
+ BASELINE_FILE_PATH,
1072
+ OCULUM_DIR,
1073
+ type BaselineData,
1074
+ type BaselineFinding,
1075
+ type DiffResult,
1076
+ type BaselineDiff,
1077
+ type BaselineManagerOptions,
1078
+ type LoadBaselineResult,
1079
+ type SaveBaselineResult,
1080
+ type ClearBaselineResult,
1081
+ } from './baseline'
1082
+
1083
+ // Rule metadata exports (PRO-82)
1084
+ export {
1085
+ RULE_REGISTRY,
1086
+ getRuleMetadata,
1087
+ getAllCategories,
1088
+ hasMetadata,
1089
+ type RuleMetadata,
1090
+ } from './rules'
@@ -211,12 +211,33 @@ export const CONFIG_RULES: ConfigRule[] = [
211
211
  // Get the package's own npm scope to detect internal monorepo deps
212
212
  const ownScope = pkg.name?.startsWith('@') ? pkg.name.split('/')[0] : null
213
213
 
214
+ // Also detect common org scope from other dependencies (for packages without scoped names)
215
+ // If we see @org/foo and @org/bar in deps, @org is likely the monorepo's scope
216
+ const scopeCounts: Record<string, number> = {}
217
+ for (const depName of Object.keys(allDeps)) {
218
+ if (depName.startsWith('@')) {
219
+ const scope = depName.split('/')[0]
220
+ scopeCounts[scope] = (scopeCounts[scope] || 0) + 1
221
+ }
222
+ }
223
+ // Find the most common scope (likely the monorepo's org scope)
224
+ const inferredScope = Object.entries(scopeCounts)
225
+ .filter(([, count]) => count >= 2) // At least 2 deps from same scope
226
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || null
227
+
214
228
  for (const [name, version] of Object.entries(allDeps)) {
215
229
  if (version === '*' || version === 'latest') {
216
- // Skip internal monorepo packages (same npm scope with * version)
217
- // In monorepos, * is used for workspace deps which are resolved locally
230
+ // Skip internal monorepo packages:
231
+ // 1. Same npm scope as the package itself
232
+ // 2. Or same scope as other workspace packages (inferred from multiple deps)
218
233
  const depScope = name.startsWith('@') ? name.split('/')[0] : null
219
- if (ownScope && depScope === ownScope && version === '*') {
234
+
235
+ // Check if this is a workspace package
236
+ const isWorkspacePackage =
237
+ (ownScope && depScope === ownScope) ||
238
+ (inferredScope && depScope === inferredScope && version === '*')
239
+
240
+ if (isWorkspacePackage) {
220
241
  continue
221
242
  }
222
243