@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,542 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * M7: Results Analysis Script
4
+ *
5
+ * Analyzes scan results from real-repo validation and generates
6
+ * summary metrics for documentation.
7
+ *
8
+ * Usage:
9
+ * npx tsx packages/scanner/src/__tests__/validation/analyze-results.ts
10
+ * npx tsx packages/scanner/src/__tests__/validation/analyze-results.ts --output docs/RESULTSCOMPARISON.md
11
+ */
12
+
13
+ import * as fs from 'fs'
14
+ import * as path from 'path'
15
+ import type { ScanResult, Vulnerability, VulnerabilityCategory, VulnerabilitySeverity } from '../../types'
16
+
17
+ const RESULTS_DIR = path.join(__dirname, '../../../validation-results')
18
+
19
+ // M5 AI-era categories we're specifically validating
20
+ const M5_CATEGORIES: VulnerabilityCategory[] = [
21
+ 'ai_rag_exfiltration',
22
+ 'ai_endpoint_unprotected',
23
+ 'ai_schema_mismatch',
24
+ 'ai_prompt_injection',
25
+ 'ai_unsafe_execution',
26
+ 'ai_overpermissive_tool',
27
+ ]
28
+
29
+ interface FindingSummary {
30
+ category: VulnerabilityCategory
31
+ count: number
32
+ bySeverity: Record<VulnerabilitySeverity, number>
33
+ examples: Array<{
34
+ file: string
35
+ line: number
36
+ title: string
37
+ severity: VulnerabilitySeverity
38
+ }>
39
+ }
40
+
41
+ interface RepoAnalysis {
42
+ repoName: string
43
+ scanDepth: string
44
+ totalFindings: number
45
+ mediumPlusFindings: number
46
+ filesScanned: number
47
+ scanDuration: number
48
+ bySeverity: Record<VulnerabilitySeverity, number>
49
+ byCategory: Record<string, number>
50
+ m5Findings: FindingSummary[]
51
+ topCategories: Array<{ category: string; count: number }>
52
+ libraryCodeFindings: number
53
+ exampleCodeFindings: number
54
+ testCodeFindings: number
55
+ }
56
+
57
+ interface ValidationReport {
58
+ generatedAt: string
59
+ totalRepos: number
60
+ totalScans: number
61
+ analyses: RepoAnalysis[]
62
+ aggregateMetrics: {
63
+ totalFindings: number
64
+ totalMediumPlus: number
65
+ avgFindingsPerRepo: number
66
+ m5DetectorPerformance: Record<string, { total: number; mediumPlus: number }>
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Classify file path as library internal, example, or test code
72
+ */
73
+ function classifyFilePath(filePath: string): 'library' | 'example' | 'test' | 'other' {
74
+ const lowerPath = filePath.toLowerCase()
75
+
76
+ // Test files
77
+ if (
78
+ lowerPath.includes('__tests__') ||
79
+ lowerPath.includes('/test/') ||
80
+ lowerPath.includes('/tests/') ||
81
+ lowerPath.includes('.test.') ||
82
+ lowerPath.includes('.spec.') ||
83
+ lowerPath.includes('_test.') ||
84
+ lowerPath.includes('/fixtures/')
85
+ ) {
86
+ return 'test'
87
+ }
88
+
89
+ // Example/demo files
90
+ if (
91
+ lowerPath.includes('/examples/') ||
92
+ lowerPath.includes('/example/') ||
93
+ lowerPath.includes('/demos/') ||
94
+ lowerPath.includes('/demo/') ||
95
+ lowerPath.includes('/templates/') ||
96
+ lowerPath.includes('/cookbook/')
97
+ ) {
98
+ return 'example'
99
+ }
100
+
101
+ // Library internal code
102
+ if (
103
+ lowerPath.includes('/src/') ||
104
+ lowerPath.includes('/lib/') ||
105
+ lowerPath.includes('/libs/') ||
106
+ lowerPath.includes('/packages/')
107
+ ) {
108
+ return 'library'
109
+ }
110
+
111
+ return 'other'
112
+ }
113
+
114
+ /**
115
+ * Load scan results from JSON files
116
+ */
117
+ function loadResults(): Map<string, ScanResult> {
118
+ const results = new Map<string, ScanResult>()
119
+
120
+ if (!fs.existsSync(RESULTS_DIR)) {
121
+ console.error(`Results directory not found: ${RESULTS_DIR}`)
122
+ console.error('Run run-validation.ts first to generate results.')
123
+ process.exit(1)
124
+ }
125
+
126
+ const files = fs.readdirSync(RESULTS_DIR).filter(f =>
127
+ f.endsWith('.json') && !f.includes('medium-plus')
128
+ )
129
+
130
+ if (files.length === 0) {
131
+ console.error('No result files found in', RESULTS_DIR)
132
+ console.error('Run run-validation.ts first to generate results.')
133
+ process.exit(1)
134
+ }
135
+
136
+ for (const file of files) {
137
+ const filePath = path.join(RESULTS_DIR, file)
138
+ const content = fs.readFileSync(filePath, 'utf-8')
139
+ const result = JSON.parse(content) as ScanResult
140
+ const key = file.replace('.json', '')
141
+ results.set(key, result)
142
+ }
143
+
144
+ return results
145
+ }
146
+
147
+ /**
148
+ * Analyze a single scan result
149
+ */
150
+ function analyzeResult(key: string, result: ScanResult): RepoAnalysis {
151
+ const [repoName, scanDepth] = key.split('-')
152
+
153
+ // Severity distribution
154
+ const bySeverity: Record<VulnerabilitySeverity, number> = {
155
+ critical: 0,
156
+ high: 0,
157
+ medium: 0,
158
+ low: 0,
159
+ info: 0,
160
+ }
161
+
162
+ // Category distribution
163
+ const byCategory: Record<string, number> = {}
164
+
165
+ // Code type distribution
166
+ let libraryCodeFindings = 0
167
+ let exampleCodeFindings = 0
168
+ let testCodeFindings = 0
169
+
170
+ // M5 detector findings
171
+ const m5FindingsMap = new Map<VulnerabilityCategory, Vulnerability[]>()
172
+ for (const cat of M5_CATEGORIES) {
173
+ m5FindingsMap.set(cat, [])
174
+ }
175
+
176
+ // Process each finding
177
+ for (const vuln of result.vulnerabilities) {
178
+ // Severity
179
+ bySeverity[vuln.severity]++
180
+
181
+ // Category
182
+ byCategory[vuln.category] = (byCategory[vuln.category] || 0) + 1
183
+
184
+ // Code type
185
+ const codeType = classifyFilePath(vuln.filePath)
186
+ if (codeType === 'library') libraryCodeFindings++
187
+ else if (codeType === 'example') exampleCodeFindings++
188
+ else if (codeType === 'test') testCodeFindings++
189
+
190
+ // M5 categories
191
+ if (M5_CATEGORIES.includes(vuln.category as VulnerabilityCategory)) {
192
+ m5FindingsMap.get(vuln.category as VulnerabilityCategory)!.push(vuln)
193
+ }
194
+ }
195
+
196
+ // Build M5 findings summary
197
+ const m5Findings: FindingSummary[] = []
198
+ for (const [category, findings] of m5FindingsMap) {
199
+ if (findings.length === 0) continue
200
+
201
+ const severityBreakdown: Record<VulnerabilitySeverity, number> = {
202
+ critical: 0,
203
+ high: 0,
204
+ medium: 0,
205
+ low: 0,
206
+ info: 0,
207
+ }
208
+
209
+ for (const f of findings) {
210
+ severityBreakdown[f.severity]++
211
+ }
212
+
213
+ // Get top 5 examples
214
+ const examples = findings.slice(0, 5).map(f => ({
215
+ file: f.filePath,
216
+ line: f.lineNumber,
217
+ title: f.title,
218
+ severity: f.severity,
219
+ }))
220
+
221
+ m5Findings.push({
222
+ category,
223
+ count: findings.length,
224
+ bySeverity: severityBreakdown,
225
+ examples,
226
+ })
227
+ }
228
+
229
+ // Sort m5Findings by count
230
+ m5Findings.sort((a, b) => b.count - a.count)
231
+
232
+ // Top categories
233
+ const topCategories = Object.entries(byCategory)
234
+ .sort(([, a], [, b]) => b - a)
235
+ .slice(0, 10)
236
+ .map(([category, count]) => ({ category, count }))
237
+
238
+ // Medium+ count
239
+ const mediumPlusFindings = bySeverity.critical + bySeverity.high + bySeverity.medium
240
+
241
+ return {
242
+ repoName,
243
+ scanDepth,
244
+ totalFindings: result.vulnerabilities.length,
245
+ mediumPlusFindings,
246
+ filesScanned: result.filesScanned,
247
+ scanDuration: result.scanDuration,
248
+ bySeverity,
249
+ byCategory,
250
+ m5Findings,
251
+ topCategories,
252
+ libraryCodeFindings,
253
+ exampleCodeFindings,
254
+ testCodeFindings,
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Generate aggregate metrics across all analyses
260
+ */
261
+ function computeAggregateMetrics(analyses: RepoAnalysis[]): ValidationReport['aggregateMetrics'] {
262
+ let totalFindings = 0
263
+ let totalMediumPlus = 0
264
+
265
+ const m5Performance: Record<string, { total: number; mediumPlus: number }> = {}
266
+ for (const cat of M5_CATEGORIES) {
267
+ m5Performance[cat] = { total: 0, mediumPlus: 0 }
268
+ }
269
+
270
+ for (const analysis of analyses) {
271
+ totalFindings += analysis.totalFindings
272
+ totalMediumPlus += analysis.mediumPlusFindings
273
+
274
+ for (const m5 of analysis.m5Findings) {
275
+ m5Performance[m5.category].total += m5.count
276
+ m5Performance[m5.category].mediumPlus +=
277
+ m5.bySeverity.critical + m5.bySeverity.high + m5.bySeverity.medium
278
+ }
279
+ }
280
+
281
+ return {
282
+ totalFindings,
283
+ totalMediumPlus,
284
+ avgFindingsPerRepo: analyses.length > 0 ? Math.round(totalFindings / analyses.length) : 0,
285
+ m5DetectorPerformance: m5Performance,
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Generate markdown report
291
+ */
292
+ function generateMarkdownReport(report: ValidationReport): string {
293
+ const lines: string[] = []
294
+
295
+ lines.push('# M7: Real-Repo Validation Results')
296
+ lines.push('')
297
+ lines.push(`> Generated: ${report.generatedAt}`)
298
+ lines.push('')
299
+
300
+ // Executive Summary
301
+ lines.push('## Executive Summary')
302
+ lines.push('')
303
+ lines.push(`- **Repositories scanned:** ${report.totalRepos}`)
304
+ lines.push(`- **Total scans:** ${report.totalScans}`)
305
+ lines.push(`- **Total findings:** ${report.aggregateMetrics.totalFindings}`)
306
+ lines.push(`- **Medium+ findings (to triage):** ${report.aggregateMetrics.totalMediumPlus}`)
307
+ lines.push('')
308
+
309
+ // M5 Detector Performance
310
+ lines.push('## M5 AI-Era Detector Performance')
311
+ lines.push('')
312
+ lines.push('| Detector | Total Findings | Medium+ |')
313
+ lines.push('|----------|----------------|---------|')
314
+
315
+ for (const [category, stats] of Object.entries(report.aggregateMetrics.m5DetectorPerformance)) {
316
+ if (stats.total > 0) {
317
+ lines.push(`| ${category} | ${stats.total} | ${stats.mediumPlus} |`)
318
+ }
319
+ }
320
+
321
+ const emptyM5 = Object.entries(report.aggregateMetrics.m5DetectorPerformance)
322
+ .filter(([, stats]) => stats.total === 0)
323
+ .map(([cat]) => cat)
324
+
325
+ if (emptyM5.length > 0) {
326
+ lines.push('')
327
+ lines.push(`**No findings for:** ${emptyM5.join(', ')}`)
328
+ }
329
+
330
+ lines.push('')
331
+
332
+ // Per-Repo Results
333
+ lines.push('## Per-Repository Results')
334
+ lines.push('')
335
+
336
+ // Group analyses by repo
337
+ const byRepo = new Map<string, RepoAnalysis[]>()
338
+ for (const analysis of report.analyses) {
339
+ const existing = byRepo.get(analysis.repoName) || []
340
+ existing.push(analysis)
341
+ byRepo.set(analysis.repoName, existing)
342
+ }
343
+
344
+ for (const [repoName, analyses] of byRepo) {
345
+ lines.push(`### ${repoName}`)
346
+ lines.push('')
347
+
348
+ for (const analysis of analyses) {
349
+ lines.push(`#### Depth: ${analysis.scanDepth}`)
350
+ lines.push('')
351
+ lines.push(`- Files scanned: ${analysis.filesScanned}`)
352
+ lines.push(`- Total findings: ${analysis.totalFindings}`)
353
+ lines.push(`- Duration: ${analysis.scanDuration}ms`)
354
+ lines.push('')
355
+ lines.push('**Severity Breakdown:**')
356
+ lines.push(`| Critical | High | Medium | Low | Info |`)
357
+ lines.push(`|----------|------|--------|-----|------|`)
358
+ lines.push(`| ${analysis.bySeverity.critical} | ${analysis.bySeverity.high} | ${analysis.bySeverity.medium} | ${analysis.bySeverity.low} | ${analysis.bySeverity.info} |`)
359
+ lines.push('')
360
+
361
+ // Code type breakdown
362
+ lines.push('**Finding Distribution by Code Type:**')
363
+ lines.push(`- Library internals: ${analysis.libraryCodeFindings}`)
364
+ lines.push(`- Examples/demos: ${analysis.exampleCodeFindings}`)
365
+ lines.push(`- Test files: ${analysis.testCodeFindings}`)
366
+ lines.push('')
367
+
368
+ // Top categories
369
+ if (analysis.topCategories.length > 0) {
370
+ lines.push('**Top Categories:**')
371
+ for (const { category, count } of analysis.topCategories.slice(0, 5)) {
372
+ lines.push(`- ${category}: ${count}`)
373
+ }
374
+ lines.push('')
375
+ }
376
+
377
+ // M5 findings with examples
378
+ if (analysis.m5Findings.length > 0) {
379
+ lines.push('**M5 Detector Findings:**')
380
+ for (const m5 of analysis.m5Findings) {
381
+ lines.push(`- **${m5.category}**: ${m5.count} findings`)
382
+ if (m5.examples.length > 0) {
383
+ for (const ex of m5.examples.slice(0, 3)) {
384
+ lines.push(` - \`${ex.file}:${ex.line}\` (${ex.severity}): ${ex.title}`)
385
+ }
386
+ }
387
+ }
388
+ lines.push('')
389
+ }
390
+ }
391
+ }
392
+
393
+ // Triage Checklist
394
+ lines.push('## FP Triage Checklist')
395
+ lines.push('')
396
+ lines.push('For each medium+ finding, classify as:')
397
+ lines.push('- [ ] **True Positive** - Real security issue')
398
+ lines.push('- [ ] **False Positive** - Safe code incorrectly flagged')
399
+ lines.push('- [ ] **Borderline** - Context-dependent, may need tuning')
400
+ lines.push('')
401
+ lines.push('### Triage Template')
402
+ lines.push('')
403
+ lines.push('```markdown')
404
+ lines.push('### Finding: [category]')
405
+ lines.push('**File:** [path]:[line]')
406
+ lines.push('**Severity:** [severity]')
407
+ lines.push('**Classification:** [ ] TP [ ] FP [ ] Borderline')
408
+ lines.push('**Context:** Library internal / Example code / Test file')
409
+ lines.push('**Reasoning:** [Why this is/isn\'t a real issue]')
410
+ lines.push('**Action:** [ ] Keep [ ] Tune detector [ ] Add to known-FP fixtures')
411
+ lines.push('```')
412
+ lines.push('')
413
+
414
+ // Next Steps
415
+ lines.push('## Next Steps')
416
+ lines.push('')
417
+ lines.push('1. Review all medium+ findings in this report')
418
+ lines.push('2. Classify each as TP/FP/Borderline')
419
+ lines.push('3. Calculate FP rate: `FP / (TP + FP)` - target: <20%')
420
+ lines.push('4. Add FP patterns to `known-false-positives.test.ts`')
421
+ lines.push('5. Tune detectors if FP rate exceeds 20%')
422
+ lines.push('')
423
+
424
+ return lines.join('\n')
425
+ }
426
+
427
+ /**
428
+ * Print console summary
429
+ */
430
+ function printConsoleSummary(report: ValidationReport): void {
431
+ console.log('\n' + '='.repeat(60))
432
+ console.log('M7: VALIDATION ANALYSIS SUMMARY')
433
+ console.log('='.repeat(60))
434
+
435
+ console.log(`\nGenerated: ${report.generatedAt}`)
436
+ console.log(`Repositories: ${report.totalRepos}`)
437
+ console.log(`Total scans: ${report.totalScans}`)
438
+
439
+ console.log('\n--- Aggregate Metrics ---')
440
+ console.log(`Total findings: ${report.aggregateMetrics.totalFindings}`)
441
+ console.log(`Medium+ findings: ${report.aggregateMetrics.totalMediumPlus} (to triage)`)
442
+ console.log(`Avg per repo: ${report.aggregateMetrics.avgFindingsPerRepo}`)
443
+
444
+ console.log('\n--- M5 Detector Performance ---')
445
+ console.log('Category | Total | Medium+')
446
+ console.log('-'.repeat(50))
447
+
448
+ for (const [category, stats] of Object.entries(report.aggregateMetrics.m5DetectorPerformance)) {
449
+ const catPadded = category.padEnd(30)
450
+ console.log(`${catPadded}| ${String(stats.total).padEnd(6)}| ${stats.mediumPlus}`)
451
+ }
452
+
453
+ console.log('\n--- Per-Repo Breakdown ---')
454
+ for (const analysis of report.analyses) {
455
+ console.log(`\n${analysis.repoName} (${analysis.scanDepth}):`)
456
+ console.log(` Files: ${analysis.filesScanned}, Findings: ${analysis.totalFindings}, Medium+: ${analysis.mediumPlusFindings}`)
457
+ console.log(` Code types: Library=${analysis.libraryCodeFindings}, Example=${analysis.exampleCodeFindings}, Test=${analysis.testCodeFindings}`)
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Main entry point
463
+ */
464
+ async function main() {
465
+ const args = process.argv.slice(2)
466
+ let outputPath: string | null = null
467
+
468
+ // Parse args
469
+ for (let i = 0; i < args.length; i++) {
470
+ if (args[i] === '--output' && args[i + 1]) {
471
+ outputPath = args[i + 1]
472
+ i++
473
+ } else if (args[i] === '--help' || args[i] === '-h') {
474
+ console.log(`
475
+ M7: Results Analysis Script
476
+
477
+ Usage:
478
+ npx tsx analyze-results.ts [options]
479
+
480
+ Options:
481
+ --output <path> Write markdown report to file
482
+ --help, -h Show this help
483
+
484
+ Examples:
485
+ npx tsx analyze-results.ts # Print summary
486
+ npx tsx analyze-results.ts --output docs/RESULTSCOMPARISON.md # Generate report
487
+ `)
488
+ process.exit(0)
489
+ }
490
+ }
491
+
492
+ // Load results
493
+ const results = loadResults()
494
+ console.log(`Loaded ${results.size} result files`)
495
+
496
+ // Analyze each result
497
+ const analyses: RepoAnalysis[] = []
498
+ for (const [key, result] of results) {
499
+ const analysis = analyzeResult(key, result)
500
+ analyses.push(analysis)
501
+ }
502
+
503
+ // Get unique repo count
504
+ const uniqueRepos = new Set(analyses.map(a => a.repoName))
505
+
506
+ // Build report
507
+ const report: ValidationReport = {
508
+ generatedAt: new Date().toISOString(),
509
+ totalRepos: uniqueRepos.size,
510
+ totalScans: analyses.length,
511
+ analyses,
512
+ aggregateMetrics: computeAggregateMetrics(analyses),
513
+ }
514
+
515
+ // Print console summary
516
+ printConsoleSummary(report)
517
+
518
+ // Generate markdown if output path specified
519
+ if (outputPath) {
520
+ const markdown = generateMarkdownReport(report)
521
+ const fullPath = path.isAbsolute(outputPath)
522
+ ? outputPath
523
+ : path.join(process.cwd(), outputPath)
524
+
525
+ // Ensure directory exists
526
+ const dir = path.dirname(fullPath)
527
+ if (!fs.existsSync(dir)) {
528
+ fs.mkdirSync(dir, { recursive: true })
529
+ }
530
+
531
+ fs.writeFileSync(fullPath, markdown)
532
+ console.log(`\nMarkdown report saved to: ${fullPath}`)
533
+ } else {
534
+ console.log('\nTip: Use --output docs/RESULTSCOMPARISON.md to generate full report')
535
+ }
536
+ }
537
+
538
+ // Run
539
+ main().catch(err => {
540
+ console.error('Analysis failed:', err)
541
+ process.exit(1)
542
+ })
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Extract medium+ findings for systematic triage
4
+ *
5
+ * Outputs findings grouped by category with file paths for code review
6
+ */
7
+
8
+ import * as fs from 'fs'
9
+ import * as path from 'path'
10
+ import type { ScanResult, Vulnerability } from '../../types'
11
+
12
+ const RESULTS_DIR = path.join(__dirname, '../../../validation-results')
13
+ const REPOS_DIR = path.join(__dirname, '../../../validation-repos')
14
+
15
+ interface TriageFinding {
16
+ id: string
17
+ repo: string
18
+ file: string
19
+ line: number
20
+ severity: string
21
+ category: string
22
+ title: string
23
+ description: string
24
+ lineContent: string
25
+ codeType: 'library' | 'example' | 'test' | 'other'
26
+ }
27
+
28
+ function classifyCodeType(filePath: string): 'library' | 'example' | 'test' | 'other' {
29
+ const lowerPath = filePath.toLowerCase()
30
+
31
+ if (lowerPath.includes('__tests__') || lowerPath.includes('/test/') ||
32
+ lowerPath.includes('/tests/') || lowerPath.includes('.test.') ||
33
+ lowerPath.includes('.spec.') || lowerPath.includes('/fixtures/')) {
34
+ return 'test'
35
+ }
36
+
37
+ if (lowerPath.includes('/examples/') || lowerPath.includes('/example/') ||
38
+ lowerPath.includes('/demos/') || lowerPath.includes('/demo/') ||
39
+ lowerPath.includes('/templates/') || lowerPath.includes('/cookbook/')) {
40
+ return 'example'
41
+ }
42
+
43
+ if (lowerPath.includes('/src/') || lowerPath.includes('/lib/') ||
44
+ lowerPath.includes('/libs/') || lowerPath.includes('/packages/')) {
45
+ return 'library'
46
+ }
47
+
48
+ return 'other'
49
+ }
50
+
51
+ function extractMediumPlusFindings(): TriageFinding[] {
52
+ const findings: TriageFinding[] = []
53
+
54
+ const files = fs.readdirSync(RESULTS_DIR).filter(f => f.endsWith('.json'))
55
+
56
+ for (const file of files) {
57
+ const repoName = file.replace('-cheap.json', '').replace('-validated.json', '')
58
+ const content = fs.readFileSync(path.join(RESULTS_DIR, file), 'utf-8')
59
+ const result = JSON.parse(content) as ScanResult
60
+
61
+ for (const vuln of result.vulnerabilities) {
62
+ if (['critical', 'high', 'medium'].includes(vuln.severity)) {
63
+ findings.push({
64
+ id: vuln.id,
65
+ repo: repoName,
66
+ file: vuln.filePath,
67
+ line: vuln.lineNumber,
68
+ severity: vuln.severity,
69
+ category: vuln.category,
70
+ title: vuln.title,
71
+ description: vuln.description,
72
+ lineContent: vuln.lineContent,
73
+ codeType: classifyCodeType(vuln.filePath),
74
+ })
75
+ }
76
+ }
77
+ }
78
+
79
+ return findings
80
+ }
81
+
82
+ function groupByCategory(findings: TriageFinding[]): Map<string, TriageFinding[]> {
83
+ const groups = new Map<string, TriageFinding[]>()
84
+
85
+ for (const f of findings) {
86
+ const existing = groups.get(f.category) || []
87
+ existing.push(f)
88
+ groups.set(f.category, existing)
89
+ }
90
+
91
+ return groups
92
+ }
93
+
94
+ function main() {
95
+ const findings = extractMediumPlusFindings()
96
+ const grouped = groupByCategory(findings)
97
+
98
+ console.log(`\n${'='.repeat(60)}`)
99
+ console.log('MEDIUM+ FINDINGS FOR TRIAGE')
100
+ console.log('='.repeat(60))
101
+ console.log(`Total: ${findings.length} findings\n`)
102
+
103
+ // Summary by category
104
+ console.log('By Category:')
105
+ const sortedCategories = Array.from(grouped.entries())
106
+ .sort((a, b) => b[1].length - a[1].length)
107
+
108
+ for (const [category, catFindings] of sortedCategories) {
109
+ const bySeverity = {
110
+ critical: catFindings.filter(f => f.severity === 'critical').length,
111
+ high: catFindings.filter(f => f.severity === 'high').length,
112
+ medium: catFindings.filter(f => f.severity === 'medium').length,
113
+ }
114
+ console.log(` ${category}: ${catFindings.length} (C:${bySeverity.critical} H:${bySeverity.high} M:${bySeverity.medium})`)
115
+ }
116
+
117
+ // By code type
118
+ const byCodeType = {
119
+ library: findings.filter(f => f.codeType === 'library').length,
120
+ example: findings.filter(f => f.codeType === 'example').length,
121
+ test: findings.filter(f => f.codeType === 'test').length,
122
+ other: findings.filter(f => f.codeType === 'other').length,
123
+ }
124
+ console.log(`\nBy Code Type:`)
125
+ console.log(` Library: ${byCodeType.library}`)
126
+ console.log(` Example: ${byCodeType.example}`)
127
+ console.log(` Test: ${byCodeType.test}`)
128
+ console.log(` Other: ${byCodeType.other}`)
129
+
130
+ // Output JSON for further processing
131
+ const outputPath = path.join(RESULTS_DIR, 'medium-plus-findings.json')
132
+ fs.writeFileSync(outputPath, JSON.stringify(findings, null, 2))
133
+ console.log(`\nFindings exported to: ${outputPath}`)
134
+
135
+ // Output summary by repo
136
+ console.log('\nBy Repository:')
137
+ const byRepo = new Map<string, number>()
138
+ for (const f of findings) {
139
+ byRepo.set(f.repo, (byRepo.get(f.repo) || 0) + 1)
140
+ }
141
+ for (const [repo, count] of byRepo) {
142
+ console.log(` ${repo}: ${count}`)
143
+ }
144
+ }
145
+
146
+ main()