thuban 0.4.0 → 0.4.2

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 (209) hide show
  1. package/dist/package.json +63 -0
  2. package/dist/packages/crucible/rules/code-scanner-core.json +129 -0
  3. package/dist/packages/crucible/rules/command-injection.json +411 -0
  4. package/dist/packages/crucible/rules/crypto-misuse.json +35 -0
  5. package/dist/packages/crucible/rules/deserialization.json +152 -0
  6. package/dist/packages/crucible/rules/env-injection.json +46 -0
  7. package/dist/packages/crucible/rules/eval-abuse.json +104 -0
  8. package/dist/packages/crucible/rules/file-upload.json +46 -0
  9. package/dist/packages/crucible/rules/hallucination.json +22 -0
  10. package/dist/packages/crucible/rules/hardcoded-secret.json +397 -0
  11. package/dist/packages/crucible/rules/hardcoded-url.json +13 -0
  12. package/dist/packages/crucible/rules/insecure-crypto.json +302 -0
  13. package/dist/packages/crucible/rules/integer-overflow.json +35 -0
  14. package/dist/packages/crucible/rules/log-injection.json +33 -0
  15. package/dist/packages/crucible/rules/mass-assignment.json +112 -0
  16. package/dist/packages/crucible/rules/open-redirect.json +196 -0
  17. package/dist/packages/crucible/rules/path-traversal.json +422 -0
  18. package/dist/packages/crucible/rules/prototype-pollution.json +22 -0
  19. package/dist/packages/crucible/rules/sql-injection.json +597 -0
  20. package/dist/packages/crucible/rules/ssrf.json +557 -0
  21. package/dist/packages/crucible/rules/timing-attack.json +55 -0
  22. package/dist/packages/crucible/rules/unsafe-block.json +24 -0
  23. package/dist/packages/crucible/rules/xss.json +641 -0
  24. package/dist/packages/crucible/rules/xxe.json +66 -0
  25. package/dist/packages/crucible/rules/yaml-deserialization.json +22 -0
  26. package/dist/packages/crucible/rules/yaml-injection.json +22 -0
  27. package/dist/packages/scanner/executive-report.js +1 -1
  28. package/dist/packages/scanner/hallucination-detector.js +1 -1
  29. package/dist/packages/scanner/secret-scanner.js +1 -1
  30. package/package.json +7 -4
  31. package/dist/packages/crucible/seeds/python/seed-021 3.py +0 -37
  32. package/dist/packages/crucible/seeds/python/seed-022 3.py +0 -34
  33. package/dist/packages/crucible/seeds/python/seed-023 3.py +0 -37
  34. package/dist/packages/crucible/seeds/python/seed-024 3.py +0 -38
  35. package/dist/packages/crucible/seeds/python/seed-025 3.py +0 -40
  36. package/dist/packages/crucible/seeds/python/seed-026 3.py +0 -35
  37. package/dist/packages/crucible/seeds/python/seed-027 3.py +0 -35
  38. package/dist/packages/crucible/seeds/python/seed-028 3.py +0 -42
  39. package/dist/packages/crucible/seeds/python/seed-030 3.py +0 -37
  40. package/dist/packages/crucible/seeds/python/seed-031 3.py +0 -34
  41. package/dist/packages/crucible/seeds/python/seed-036 3.py +0 -33
  42. package/dist/packages/crucible/seeds/python/seed-037 3.py +0 -41
  43. package/dist/packages/crucible/seeds/python/seed-038 3.py +0 -33
  44. package/dist/packages/crucible/seeds/python/seed-039 3.py +0 -39
  45. package/dist/packages/crucible/seeds/python/seed-040 3.py +0 -39
  46. package/dist/packages/crucible/seeds/python/seed-041 3.py +0 -37
  47. package/dist/packages/crucible/seeds/python/seed-042 3.py +0 -38
  48. package/dist/packages/crucible/seeds/python/seed-043 3.py +0 -32
  49. package/dist/packages/crucible/seeds/python/seed-044 3.py +0 -38
  50. package/dist/packages/crucible/seeds/python/seed-045 3.py +0 -36
  51. package/dist/packages/crucible/seeds/python/seed-046 3.py +0 -33
  52. package/dist/packages/crucible/seeds/python/seed-047 3.py +0 -44
  53. package/dist/packages/crucible/seeds/python/seed-048 3.py +0 -35
  54. package/dist/packages/crucible/seeds/python/seed-049 3.py +0 -39
  55. package/dist/packages/crucible/seeds/python/seed-050 3.py +0 -39
  56. package/dist/packages/crucible/seeds/python/seed-051 3.py +0 -38
  57. package/dist/packages/crucible/seeds/python/seed-052 3.py +0 -41
  58. package/dist/packages/crucible/seeds/ruby/seed-001 3.rb +0 -15
  59. package/dist/packages/crucible/seeds/ruby/seed-002 3.rb +0 -22
  60. package/dist/packages/crucible/seeds/ruby/seed-003 3.rb +0 -25
  61. package/dist/packages/crucible/seeds/ruby/seed-004 3.rb +0 -17
  62. package/dist/packages/crucible/seeds/ruby/seed-005 3.rb +0 -21
  63. package/dist/packages/crucible/seeds/ruby/seed-006 3.rb +0 -17
  64. package/dist/packages/crucible/seeds/ruby/seed-007 3.rb +0 -16
  65. package/dist/packages/crucible/seeds/ruby/seed-008 3.rb +0 -18
  66. package/dist/packages/crucible/seeds/ruby/seed-009 3.rb +0 -20
  67. package/dist/packages/crucible/seeds/ruby/seed-010 3.rb +0 -24
  68. package/dist/packages/crucible/seeds/ruby/seed-011 3.rb +0 -21
  69. package/dist/packages/crucible/seeds/ruby/seed-012 3.rb +0 -22
  70. package/dist/packages/crucible/seeds/ruby/seed-013 3.rb +0 -21
  71. package/dist/packages/crucible/seeds/ruby/seed-014 3.rb +0 -16
  72. package/dist/packages/crucible/seeds/ruby/seed-015 3.rb +0 -18
  73. package/dist/packages/crucible/seeds/ruby/seed-016 3.rb +0 -17
  74. package/dist/packages/crucible/seeds/ruby/seed-017 3.rb +0 -25
  75. package/dist/packages/crucible/seeds/ruby/seed-018 3.rb +0 -23
  76. package/dist/packages/crucible/seeds/ruby/seed-019 3.rb +0 -20
  77. package/dist/packages/crucible/seeds/ruby/seed-020 3.rb +0 -17
  78. package/dist/packages/crucible/seeds/ruby/seed-021 3.rb +0 -20
  79. package/dist/packages/crucible/seeds/ruby/seed-022 3.rb +0 -21
  80. package/dist/packages/crucible/seeds/ruby/seed-023 3.rb +0 -19
  81. package/dist/packages/crucible/seeds/ruby/seed-024 3.rb +0 -17
  82. package/dist/packages/crucible/seeds/ruby/seed-025 3.rb +0 -17
  83. package/dist/packages/crucible/seeds/ruby/seed-026 3.rb +0 -18
  84. package/dist/packages/crucible/seeds/ruby/seed-027 3.rb +0 -21
  85. package/dist/packages/crucible/seeds/ruby/seed-028 3.rb +0 -22
  86. package/dist/packages/crucible/seeds/ruby/seed-029 3.rb +0 -19
  87. package/dist/packages/crucible/seeds/ruby/seed-030 3.rb +0 -20
  88. package/dist/packages/crucible/seeds/ruby/seed-031 3.rb +0 -17
  89. package/dist/packages/crucible/seeds/ruby/seed-032 3.rb +0 -22
  90. package/dist/packages/crucible/seeds/ruby/seed-033 3.rb +0 -18
  91. package/dist/packages/crucible/seeds/ruby/seed-034 3.rb +0 -19
  92. package/dist/packages/crucible/seeds/ruby/seed-035 3.rb +0 -19
  93. package/dist/packages/crucible/seeds/ruby/seed-036 3.rb +0 -18
  94. package/dist/packages/crucible/seeds/ruby/seed-037 3.rb +0 -21
  95. package/dist/packages/crucible/seeds/ruby/seed-038 3.rb +0 -24
  96. package/dist/packages/crucible/seeds/ruby/seed-039 3.rb +0 -24
  97. package/dist/packages/crucible/seeds/ruby/seed-040 3.rb +0 -22
  98. package/dist/packages/crucible/seeds/ruby/seed-041 3.rb +0 -23
  99. package/dist/packages/crucible/seeds/ruby/seed-042 3.rb +0 -25
  100. package/dist/packages/crucible/seeds/ruby/seed-043 3.rb +0 -23
  101. package/dist/packages/crucible/seeds/ruby/seed-044 3.rb +0 -16
  102. package/dist/packages/crucible/seeds/ruby/seed-045 3.rb +0 -22
  103. package/dist/packages/crucible/seeds/ruby/seed-046 3.rb +0 -27
  104. package/dist/packages/crucible/seeds/ruby/seed-047 3.rb +0 -26
  105. package/dist/packages/crucible/seeds/ruby/seed-048 3.rb +0 -24
  106. package/dist/packages/crucible/seeds/ruby/seed-049 3.rb +0 -20
  107. package/dist/packages/crucible/seeds/ruby/seed-050 3.rb +0 -27
  108. package/dist/packages/crucible/seeds/rust/seed-001 3.rs +0 -25
  109. package/dist/packages/crucible/seeds/rust/seed-002 3.rs +0 -20
  110. package/dist/packages/crucible/seeds/rust/seed-003 3.rs +0 -23
  111. package/dist/packages/crucible/seeds/rust/seed-004 3.rs +0 -21
  112. package/dist/packages/crucible/seeds/rust/seed-005 3.rs +0 -21
  113. package/dist/packages/crucible/seeds/rust/seed-006 3.rs +0 -24
  114. package/dist/packages/crucible/seeds/rust/seed-007 3.rs +0 -20
  115. package/dist/packages/crucible/seeds/rust/seed-008 3.rs +0 -19
  116. package/dist/packages/crucible/seeds/rust/seed-009 3.rs +0 -28
  117. package/dist/packages/crucible/seeds/rust/seed-010 3.rs +0 -28
  118. package/dist/packages/crucible/seeds/rust/seed-011 3.rs +0 -25
  119. package/dist/packages/crucible/seeds/rust/seed-012 3.rs +0 -31
  120. package/dist/packages/crucible/seeds/rust/seed-013 3.rs +0 -27
  121. package/dist/packages/crucible/seeds/rust/seed-014 3.rs +0 -30
  122. package/dist/packages/crucible/seeds/rust/seed-015 3.rs +0 -33
  123. package/dist/packages/crucible/seeds/rust/seed-016 3.rs +0 -22
  124. package/dist/packages/crucible/seeds/rust/seed-017 3.rs +0 -28
  125. package/dist/packages/crucible/seeds/rust/seed-018 3.rs +0 -21
  126. package/dist/packages/crucible/seeds/rust/seed-019 3.rs +0 -36
  127. package/dist/packages/crucible/seeds/rust/seed-020 3.rs +0 -27
  128. package/dist/packages/crucible/seeds/rust/seed-021 3.rs +0 -26
  129. package/dist/packages/crucible/seeds/rust/seed-022 3.rs +0 -23
  130. package/dist/packages/crucible/seeds/rust/seed-023 3.rs +0 -22
  131. package/dist/packages/crucible/seeds/rust/seed-024 3.rs +0 -24
  132. package/dist/packages/crucible/seeds/rust/seed-025 3.rs +0 -29
  133. package/dist/packages/crucible/seeds/rust/seed-026 3.rs +0 -23
  134. package/dist/packages/crucible/seeds/rust/seed-027 3.rs +0 -24
  135. package/dist/packages/crucible/seeds/rust/seed-028 3.rs +0 -25
  136. package/dist/packages/crucible/seeds/rust/seed-029 3.rs +0 -25
  137. package/dist/packages/crucible/seeds/rust/seed-030 3.rs +0 -30
  138. package/dist/packages/crucible/seeds/rust/seed-031 3.rs +0 -22
  139. package/dist/packages/crucible/seeds/rust/seed-032 3.rs +0 -25
  140. package/dist/packages/crucible/seeds/rust/seed-033 3.rs +0 -25
  141. package/dist/packages/crucible/seeds/rust/seed-034 3.rs +0 -20
  142. package/dist/packages/crucible/seeds/rust/seed-035 3.rs +0 -28
  143. package/dist/packages/crucible/seeds/rust/seed-036 3.rs +0 -26
  144. package/dist/packages/crucible/seeds/rust/seed-037 3.rs +0 -31
  145. package/dist/packages/crucible/seeds/rust/seed-038 3.rs +0 -25
  146. package/dist/packages/crucible/seeds/rust/seed-039 3.rs +0 -28
  147. package/dist/packages/crucible/seeds/rust/seed-040 3.rs +0 -27
  148. package/dist/packages/crucible/seeds/rust/seed-041 3.rs +0 -32
  149. package/dist/packages/crucible/seeds/rust/seed-042 3.rs +0 -27
  150. package/dist/packages/crucible/seeds/rust/seed-043 3.rs +0 -29
  151. package/dist/packages/crucible/seeds/rust/seed-044 3.rs +0 -25
  152. package/dist/packages/crucible/seeds/rust/seed-045 3.rs +0 -28
  153. package/dist/packages/crucible/seeds/rust/seed-046 3.rs +0 -25
  154. package/dist/packages/crucible/seeds/rust/seed-047 3.rs +0 -34
  155. package/dist/packages/crucible/seeds/rust/seed-048 3.rs +0 -21
  156. package/dist/packages/crucible/seeds/rust/seed-049 3.rs +0 -26
  157. package/dist/packages/crucible/seeds/rust/seed-050 3.rs +0 -23
  158. package/dist/packages/crucible/seeds/ts/seed-001 3.ts +0 -32
  159. package/dist/packages/crucible/seeds/ts/seed-002 3.ts +0 -34
  160. package/dist/packages/crucible/seeds/ts/seed-003 3.ts +0 -28
  161. package/dist/packages/crucible/seeds/ts/seed-004 3.ts +0 -34
  162. package/dist/packages/crucible/seeds/ts/seed-005 3.ts +0 -32
  163. package/dist/packages/crucible/seeds/ts/seed-006 3.ts +0 -31
  164. package/dist/packages/crucible/seeds/ts/seed-007 3.ts +0 -28
  165. package/dist/packages/crucible/seeds/ts/seed-008 3.ts +0 -40
  166. package/dist/packages/crucible/seeds/ts/seed-009 3.ts +0 -31
  167. package/dist/packages/crucible/seeds/ts/seed-010 3.ts +0 -33
  168. package/dist/packages/crucible/seeds/ts/seed-011 3.ts +0 -29
  169. package/dist/packages/crucible/seeds/ts/seed-012 3.ts +0 -34
  170. package/dist/packages/crucible/seeds/ts/seed-013 3.ts +0 -31
  171. package/dist/packages/crucible/seeds/ts/seed-014 3.ts +0 -36
  172. package/dist/packages/crucible/seeds/ts/seed-015 3.ts +0 -31
  173. package/dist/packages/crucible/seeds/ts/seed-016 3.ts +0 -37
  174. package/dist/packages/crucible/seeds/ts/seed-017 3.ts +0 -44
  175. package/dist/packages/crucible/seeds/ts/seed-018 3.ts +0 -33
  176. package/dist/packages/crucible/seeds/ts/seed-019 3.ts +0 -32
  177. package/dist/packages/crucible/seeds/ts/seed-020 3.ts +0 -33
  178. package/dist/packages/crucible/seeds/ts/seed-021 3.ts +0 -33
  179. package/dist/packages/crucible/seeds/ts/seed-022 3.ts +0 -34
  180. package/dist/packages/crucible/seeds/ts/seed-023 3.ts +0 -33
  181. package/dist/packages/crucible/seeds/ts/seed-024 3.ts +0 -35
  182. package/dist/packages/crucible/seeds/ts/seed-025 3.ts +0 -29
  183. package/dist/packages/crucible/seeds/ts/seed-026 3.ts +0 -36
  184. package/dist/packages/crucible/seeds/ts/seed-027 3.ts +0 -30
  185. package/dist/packages/crucible/seeds/ts/seed-028 3.ts +0 -32
  186. package/dist/packages/crucible/seeds/ts/seed-029 3.ts +0 -34
  187. package/dist/packages/crucible/seeds/ts/seed-030 3.ts +0 -37
  188. package/dist/packages/crucible/seeds/ts/seed-031 3.ts +0 -31
  189. package/dist/packages/crucible/seeds/ts/seed-032 3.ts +0 -32
  190. package/dist/packages/crucible/seeds/ts/seed-033 3.ts +0 -32
  191. package/dist/packages/crucible/seeds/ts/seed-034 3.ts +0 -34
  192. package/dist/packages/crucible/seeds/ts/seed-035 3.ts +0 -29
  193. package/dist/packages/crucible/seeds/ts/seed-036 3.ts +0 -34
  194. package/dist/packages/crucible/seeds/ts/seed-037 3.ts +0 -33
  195. package/dist/packages/crucible/seeds/ts/seed-038 3.ts +0 -29
  196. package/dist/packages/crucible/seeds/ts/seed-039 3.ts +0 -35
  197. package/dist/packages/crucible/seeds/ts/seed-040 3.ts +0 -31
  198. package/dist/packages/crucible/seeds/ts/seed-041 3.ts +0 -29
  199. package/dist/packages/crucible/seeds/ts/seed-042 3.ts +0 -33
  200. package/dist/packages/crucible/seeds/ts/seed-043 3.ts +0 -32
  201. package/dist/packages/crucible/seeds/ts/seed-044 3.ts +0 -36
  202. package/dist/packages/crucible/seeds/ts/seed-045 3.ts +0 -39
  203. package/dist/packages/crucible/seeds/ts/seed-046 3.ts +0 -41
  204. package/dist/packages/crucible/seeds/ts/seed-047 3.ts +0 -33
  205. package/dist/packages/crucible/seeds/ts/seed-048 3.ts +0 -33
  206. package/dist/packages/crucible/seeds/ts/seed-049 3.ts +0 -34
  207. package/dist/packages/crucible/seeds/ts/seed-050 3.ts +0 -41
  208. package/dist/packages/crucible/seeds/ts/seed-051 3.ts +0 -34
  209. package/dist/packages/crucible/seeds/ts/seed-052 3.ts +0 -36
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "id": "YAMLD001",
4
+ "pattern": "YAML\\.load\\s*\\(",
5
+ "flags": "",
6
+ "category": "yaml_deserialization",
7
+ "description": "YAML deserialization",
8
+ "language": [
9
+ "js",
10
+ "ts",
11
+ "python",
12
+ "java",
13
+ "go",
14
+ "csharp",
15
+ "php",
16
+ "ruby",
17
+ "rust",
18
+ "kotlin"
19
+ ],
20
+ "severity": "medium"
21
+ }
22
+ ]
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "id": "YAML001",
4
+ "pattern": "yaml\\.load\\s*\\((?![^)]*[Ll]oader\\s*=\\s*yaml\\.(?:Safe|Base)Loader)|YAML\\.load\\s*\\(",
5
+ "flags": "i",
6
+ "category": "yaml_injection",
7
+ "description": "YAML injection (yaml.load without safe_load)",
8
+ "language": [
9
+ "js",
10
+ "ts",
11
+ "python",
12
+ "java",
13
+ "go",
14
+ "csharp",
15
+ "php",
16
+ "ruby",
17
+ "rust",
18
+ "kotlin"
19
+ ],
20
+ "severity": "medium"
21
+ }
22
+ ]
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),crypto=require("crypto");class ExecutiveReport{constructor(n={}){this.rootPath=n.rootPath||process.cwd(),this.companyName=n.companyName||this._detectProjectName(),this.generatedAt=new Date}_detectProjectName(){try{return JSON.parse(fs.readFileSync(path.join(this.rootPath,"package.json"),"utf-8")).name||path.basename(this.rootPath)}catch{return path.basename(this.rootPath)}}generate(n){const{scanResults:e={},hallResults:t={},debtCost:o={},ghostResults:s={},aiScoreResults:a={},cloneResults:r={},passportData:i={},fileCount:l=0,lineCount:d=0}=n,c=`THB-${Date.now().toString(36).toUpperCase()}`,h=this._calculateOverallScore(n),m=this._scoreToGrade(h),g=this._gradeColor(m);return this._renderHTML({reportId:c,score:h,grade:m,gradeColor:g,scanResults:e,hallResults:t,debtCost:o,ghostResults:s,aiScoreResults:a,cloneResults:r,passportData:i,fileCount:l,lineCount:d})}_calculateOverallScore(n){let e=100;const t=n.hallResults||{},o=n.ghostResults||{},s=n.cloneResults||{},a=n.aiScoreResults||{},r=n.scanResults||{};e-=15*(t.phantomAPIs?.length||0),e-=5*(t.deprecatedAPIs?.length||0),e-=2*(o.totalGhosts||0),e-=3*(s.totalClusters||0),e-=1*(a.aiLikelyCount||0);let i=0;if(r&&"object"==typeof r)for(const[,n]of Object.entries(r))n?.issues&&(i+=n.issues.length);return e-=Math.min(.5*i,30),Math.max(0,Math.min(100,Math.round(e)))}_scoreToGrade(n){return n>=90?"A":n>=80?"B":n>=65?"C":n>=50?"D":"F"}_gradeColor(n){return{A:"#00ff88",B:"#88ff00",C:"#ffd93d",D:"#ff8844",F:"#ff4444"}[n]||"#ff4444"}_renderHTML(n){const e=this.generatedAt,t=e.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"}),o=e.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}),s=n.hallResults.phantomAPIs?.length||0,a=n.hallResults.deprecatedAPIs?.length||0,r=n.ghostResults.totalGhosts||0,i=n.ghostResults.totalWastedLines||0,l=n.cloneResults.totalClusters||0,d=n.cloneResults.totalDuplicateLines||0,c=n.aiScoreResults.aiLikelyCount||0,h=n.aiScoreResults.aiPossibleCount||0,m=n.aiScoreResults.aiPercentage||0,g=n.debtCost.totalHoursManual||0,v=n.debtCost.totalCostManual||0,u=n.debtCost.totalHoursThuban||0,p=n.debtCost.totalSaving||0,f=n.debtCost.savingPercent||0,b=n.debtCost.weeklyDebtAccumulation||.7,y=Math.round(4*b*80);let x="";if(n.hallResults.phantomAPIs)for(const e of n.hallResults.phantomAPIs.slice(0,8))x+=`<tr>\n <td class="sev-critical">CRITICAL</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Phantom API")}</td>\n <td>Will crash at runtime — method does not exist</td>\n </tr>`;if(n.hallResults.deprecatedAPIs)for(const e of n.hallResults.deprecatedAPIs.slice(0,5))x+=`<tr>\n <td class="sev-high">HIGH</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Deprecated API")}</td>\n <td>Deprecated — will break in future versions</td>\n </tr>`;let $="";if(n.ghostResults.ghosts)for(const e of n.ghostResults.ghosts.slice(0,8))$+=`<tr>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.wastedLines} lines</td>\n <td>${e.references} references</td>\n </tr>`;let w="";if(n.aiScoreResults.highConfidence)for(const e of n.aiScoreResults.highConfidence.slice(0,8))w+=`<tr>\n <td class="sev-critical">${e.confidence}%</td>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.lineCount} lines</td>\n </tr>`;let C="";if(n.cloneResults.clusters)for(const e of n.cloneResults.clusters.slice(0,6)){const n=e.members.map(n=>`${this._esc(n.file)}:${n.line}`).join("<br>");C+=`<tr>\n <td>${this._esc(e.pattern)}</td>\n <td>${e.totalInstances}</td>\n <td style="font-size:0.75rem">${n}</td>\n <td>${this._esc(e.recommendation)}</td>\n </tr>`}return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Thuban Code Health Report — ${this._esc(this.companyName)}</title>\n<style>\n @page { size: A4; margin: 15mm; }\n @media print {\n body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .no-print { display: none !important; }\n .page-break { page-break-before: always; }\n }\n\n :root {\n --bg: #0a0a1a;\n --surface: #12122a;\n --border: #2a2a5a;\n --text: #e0e0f0;\n --dim: #8888aa;\n --cyan: #00f6ff;\n --green: #00ff88;\n --yellow: #ffd93d;\n --red: #ff4444;\n --mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;\n --sans: 'Segoe UI', system-ui, -apple-system, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 11pt;\n line-height: 1.5;\n max-width: 210mm;\n margin: 0 auto;\n padding: 20mm 15mm;\n }\n\n /* Header */\n .report-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n border-bottom: 2px solid var(--cyan);\n padding-bottom: 1.5rem;\n margin-bottom: 2rem;\n }\n\n .report-header .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .report-header .logo svg { width: 36px; height: 36px; }\n\n .report-header .logo-text {\n font-size: 1.5rem;\n font-weight: 300;\n letter-spacing: 0.3em;\n color: var(--cyan);\n }\n\n .report-header .meta {\n text-align: right;\n font-size: 0.8rem;\n color: var(--dim);\n line-height: 1.8;\n }\n\n .report-title {\n text-align: center;\n margin-bottom: 2.5rem;\n }\n\n .report-title h1 {\n font-size: 1.8rem;\n font-weight: 700;\n margin-bottom: 0.25rem;\n }\n\n .report-title .project-name {\n color: var(--cyan);\n font-size: 1.1rem;\n font-family: var(--mono);\n }\n\n /* Score Ring */\n .score-section {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 3rem;\n margin-bottom: 2.5rem;\n padding: 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n\n .score-ring {\n position: relative;\n width: 140px;\n height: 140px;\n }\n\n .score-ring svg { transform: rotate(-90deg); }\n\n .score-ring .score-value {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .score-ring .grade {\n font-size: 2.5rem;\n font-weight: 700;\n font-family: var(--mono);\n line-height: 1;\n }\n\n .score-ring .number {\n font-size: 0.85rem;\n color: var(--dim);\n }\n\n .score-summary {\n flex: 1;\n }\n\n .score-summary h2 {\n font-size: 1.3rem;\n margin-bottom: 0.75rem;\n }\n\n .stat-row {\n display: flex;\n gap: 2rem;\n flex-wrap: wrap;\n margin-top: 1rem;\n }\n\n .stat {\n text-align: center;\n min-width: 80px;\n }\n\n .stat .stat-value {\n font-size: 1.5rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .stat .stat-label {\n font-size: 0.7rem;\n color: var(--dim);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n .stat-critical .stat-value { color: var(--red); }\n .stat-warning .stat-value { color: var(--yellow); }\n .stat-good .stat-value { color: var(--green); }\n .stat-info .stat-value { color: var(--cyan); }\n\n /* Sections */\n .section {\n margin-bottom: 2rem;\n }\n\n .section-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n }\n\n .section-header h3 {\n font-size: 1rem;\n font-weight: 600;\n }\n\n .section-header .icon { font-size: 1.2rem; }\n\n /* Tables */\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n padding: 0.5rem;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-weight: 600;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n }\n\n td {\n padding: 0.4rem 0.5rem;\n border-bottom: 1px solid rgba(42,42,90,0.3);\n font-family: var(--mono);\n font-size: 0.75rem;\n word-break: break-all;\n }\n\n .sev-critical { color: var(--red); font-weight: 700; }\n .sev-high { color: var(--yellow); font-weight: 600; }\n\n /* Cost Box */\n .cost-box {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 1rem;\n }\n\n .cost-grid {\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 1.5rem;\n text-align: center;\n }\n\n .cost-item .cost-value {\n font-size: 1.8rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .cost-item .cost-label {\n font-size: 0.75rem;\n color: var(--dim);\n margin-top: 0.25rem;\n }\n\n .cost-manual .cost-value { color: var(--red); }\n .cost-thuban .cost-value { color: var(--cyan); }\n .cost-saving .cost-value { color: var(--green); }\n\n .roi-bar {\n margin-top: 1.5rem;\n padding: 1rem;\n background: rgba(0,255,136,0.08);\n border: 1px solid rgba(0,255,136,0.2);\n border-radius: 8px;\n text-align: center;\n font-size: 0.85rem;\n }\n\n .roi-bar strong { color: var(--green); }\n\n /* Warning callout */\n .callout {\n padding: 1rem 1.5rem;\n border-radius: 8px;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .callout-danger {\n background: rgba(255,68,68,0.08);\n border: 1px solid rgba(255,68,68,0.2);\n color: #ffaaaa;\n }\n\n .callout-warning {\n background: rgba(255,217,61,0.08);\n border: 1px solid rgba(255,217,61,0.2);\n color: #ffe088;\n }\n\n /* Footer */\n .report-footer {\n margin-top: 3rem;\n padding-top: 1.5rem;\n border-top: 1px solid var(--border);\n display: flex;\n justify-content: space-between;\n font-size: 0.75rem;\n color: var(--dim);\n }\n\n .report-footer a { color: var(--cyan); text-decoration: none; }\n\n /* Print button */\n .print-btn {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n background: var(--cyan);\n color: var(--bg);\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 8px;\n font-weight: 600;\n font-size: 0.9rem;\n cursor: pointer;\n box-shadow: 0 4px 20px rgba(0,246,255,0.3);\n z-index: 100;\n }\n\n .print-btn:hover { background: #33f8ff; }\n\n /* Responsive for screen viewing */\n @media screen and (max-width: 700px) {\n body { padding: 1rem; }\n .cost-grid { grid-template-columns: 1fr; }\n .stat-row { flex-direction: column; gap: 0.75rem; }\n .score-section { flex-direction: column; gap: 1.5rem; }\n }\n</style>\n</head>\n<body>\n\n<button class="print-btn no-print" onclick="window.print()">Save as PDF</button>\n\n\x3c!-- Header --\x3e\n<div class="report-header">\n <div class="logo">\n <svg viewBox="0 0 32 32" fill="none">\n <polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>\n <polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>\n <circle cx="16" cy="18" r="3" fill="#00f6ff"/>\n </svg>\n <span class="logo-text">THUBAN</span>\n </div>\n <div class="meta">\n Report ID: ${n.reportId}<br>\n Generated: ${t} at ${o}<br>\n Engine: Thuban Code Health v0.3.0\n </div>\n</div>\n\n\x3c!-- Title --\x3e\n<div class="report-title">\n <h1>Code Health Report</h1>\n <div class="project-name">${this._esc(this.companyName)}</div>\n</div>\n\n\x3c!-- Score --\x3e\n<div class="score-section">\n <div class="score-ring">\n <svg width="140" height="140" viewBox="0 0 140 140">\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}22" stroke-width="10"/>\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}" stroke-width="10"\n stroke-dasharray="${Math.round(3.77*n.score)} 377"\n stroke-linecap="round"/>\n </svg>\n <div class="score-value">\n <div class="grade" style="color:${n.gradeColor}">${n.grade}</div>\n <div class="number">${n.score}/100</div>\n </div>\n </div>\n <div class="score-summary">\n <h2>Codebase Health: ${"A"===n.grade?"Excellent":"B"===n.grade?"Good":"C"===n.grade?"Needs Attention":"D"===n.grade?"At Risk":"Critical"}</h2>\n <div class="stat-row">\n <div class="stat stat-info">\n <div class="stat-value">${n.fileCount.toLocaleString()}</div>\n <div class="stat-label">Files Scanned</div>\n </div>\n <div class="stat stat-info">\n <div class="stat-value">${n.lineCount.toLocaleString()}</div>\n <div class="stat-label">Lines of Code</div>\n </div>\n <div class="stat ${s>0?"stat-critical":"stat-good"}">\n <div class="stat-value">${s}</div>\n <div class="stat-label">Hallucinated APIs</div>\n </div>\n <div class="stat ${r>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${r}</div>\n <div class="stat-label">Ghost Functions</div>\n </div>\n <div class="stat ${c>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${m}%</div>\n <div class="stat-label">AI-Generated</div>\n </div>\n </div>\n </div>\n</div>\n\n${s>0?`\n<div class="callout callout-danger">\n <strong>PRODUCTION RISK:</strong> ${s} API call${s>1?"s":""} in this codebase reference${1===s?"s":""} methods that do not exist.\n These will throw runtime errors in production. This is not tech debt — this is code that <strong>will crash</strong>.\n</div>\n`:""}\n\n\x3c!-- Tech Debt Cost --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128176;</span>\n <h3>Technical Debt Cost Analysis</h3>\n </div>\n <div class="cost-box">\n <div class="cost-grid">\n <div class="cost-item cost-manual">\n <div class="cost-value">&pound;${v.toLocaleString()}</div>\n <div class="cost-label">Manual fix cost (${g} dev-hours at &pound;80/hr)</div>\n </div>\n <div class="cost-item cost-thuban">\n <div class="cost-value">${u}h</div>\n <div class="cost-label">Fix time with Thuban auto-fix</div>\n </div>\n <div class="cost-item cost-saving">\n <div class="cost-value">&pound;${p.toLocaleString()}</div>\n <div class="cost-label">You save (${f}%)</div>\n </div>\n </div>\n <div class="roi-bar">\n Every week this debt goes unaddressed, <strong>${b} more hours</strong> accumulate.\n That is <strong>&pound;${y.toLocaleString()}/month</strong> in growing technical debt.\n Thuban Pro costs &pound;19/month. <strong>ROI: ${v>0?Math.round(v/19):0}x.</strong>\n </div>\n </div>\n</div>\n\n${s+a>0?`\n\x3c!-- Hallucinations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128302;</span>\n <h3>AI Hallucination Detection</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n These API calls reference methods, modules, or endpoints that do not exist in the libraries being used.\n They were likely generated by an AI coding tool and will fail at runtime.\n </p>\n <table>\n <thead>\n <tr><th>Severity</th><th>Location</th><th>API Call</th><th>Impact</th></tr>\n </thead>\n <tbody>${x}</tbody>\n </table>\n</div>\n`:""}\n\n${r>0?`\n<div class="page-break"></div>\n\x3c!-- Ghost Code --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128123;</span>\n <h3>Ghost Code — Dead Functions</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${r} function${r>1?"s":""} exist in the codebase but are never called.\n This represents ${i.toLocaleString()} wasted lines of code (${n.ghostResults.wastedPercent||"?"}% of the codebase).\n </p>\n <table>\n <thead>\n <tr><th>Location</th><th>Function</th><th>Size</th><th>References</th></tr>\n </thead>\n <tbody>${$}</tbody>\n </table>\n</div>\n`:""}\n\n${c>0?`\n\x3c!-- AI Confidence --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#129302;</span>\n <h3>AI-Generated Code Confidence</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${m}% of functions show strong signals of AI generation.\n ${c} high confidence, ${h} possible.\n These functions should be reviewed for correctness, edge cases, and test coverage.\n </p>\n <table>\n <thead>\n <tr><th>AI %</th><th>Location</th><th>Function</th><th>Size</th></tr>\n </thead>\n <tbody>${w}</tbody>\n </table>\n</div>\n`:""}\n\n${l>0?`\n\x3c!-- Copy-Paste Drift --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128203;</span>\n <h3>Copy-Paste Drift</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${l} cluster${l>1?"s":""} of similar code found across ${d} duplicate lines.\n These indicate copy-pasted logic that has drifted apart and should be extracted into shared utilities.\n </p>\n <table>\n <thead>\n <tr><th>Pattern</th><th>Instances</th><th>Locations</th><th>Recommendation</th></tr>\n </thead>\n <tbody>${C}</tbody>\n </table>\n</div>\n`:""}\n\n\x3c!-- Recommendations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#9889;</span>\n <h3>Recommended Actions</h3>\n </div>\n <div style="display:grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; font-size:0.85rem;">\n ${s>0?`\n <div style="color:var(--red); font-weight:700;">1. IMMEDIATE</div>\n <div>Fix ${s} hallucinated API${s>1?"s":""} — these will crash in production. Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban fix . --fix</code></div>\n `:""}\n ${r>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"2":"1"}. HIGH</div>\n <div>Remove ${r} ghost function${r>1?"s":""} (${i} wasted lines). Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban ghosts .</code> for full list</div>\n `:""}\n ${l>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"3":"2"}. MEDIUM</div>\n <div>Consolidate ${l} copy-paste cluster${l>1?"s":""} into shared utilities</div>\n `:""}\n ${c>0?`\n <div style="color:var(--cyan); font-weight:700;">${s>0?"4":"3"}. REVIEW</div>\n <div>Audit ${c} high-confidence AI-generated function${c>1?"s":""} for correctness and test coverage</div>\n `:""}\n <div style="color:var(--green); font-weight:700;">ONGOING</div>\n <div>Install pre-commit gate: <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban gate --fix</code> — blocks hallucinated code from entering your repo</div>\n </div>\n</div>\n\n\x3c!-- Footer --\x3e\n<div class="report-footer">\n <div>\n <strong>Thuban</strong> — Code Health Engine<br>\n <a href="https://thuban.dev">thuban.dev</a> &nbsp;|&nbsp; A Silverwings product\n </div>\n <div style="text-align:right;">\n Report: ${n.reportId}<br>\n This report was generated automatically.<br>\n For full details, run: <code style="font-family:var(--mono); color:var(--cyan);">npx thuban scan .</code>\n </div>\n</div>\n\n</body>\n</html>`}_esc(n){return String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")}save(n,e){const t=e||this.rootPath,o=`thuban-report-${this.generatedAt.toISOString().split("T")[0]}.html`,s=path.join(t,o);return fs.writeFileSync(s,n,"utf-8"),s}}module.exports=ExecutiveReport;
1
+ const fs=require("fs"),path=require("path"),crypto=require("crypto");class ExecutiveReport{constructor(n={}){this.rootPath=n.rootPath||process.cwd(),this.companyName=n.companyName||this._detectProjectName(),this.generatedAt=new Date}_detectProjectName(){try{return JSON.parse(fs.readFileSync(path.join(this.rootPath,"package.json"),"utf-8")).name||path.basename(this.rootPath)}catch{return path.basename(this.rootPath)}}generate(n){const{scanResults:e={},hallResults:t={},debtCost:o={},ghostResults:s={},aiScoreResults:r={},cloneResults:a={},passportData:i={},fileCount:l=0,lineCount:d=0}=n,c=`THB-${Date.now().toString(36).toUpperCase()}`,h=this._calculateOverallScore(n),m=this._scoreToGrade(h),g=this._gradeColor(m);return this._renderHTML({reportId:c,score:h,grade:m,gradeColor:g,scanResults:e,hallResults:t,debtCost:o,ghostResults:s,aiScoreResults:r,cloneResults:a,passportData:i,fileCount:l,lineCount:d})}_calculateOverallScore(n){let e=100;const t=n.hallResults||{},o=n.ghostResults||{},s=n.cloneResults||{},r=n.aiScoreResults||{},a=n.scanResults||{};e-=15*(t.phantomAPIs?.length||0),e-=5*(t.deprecatedAPIs?.length||0),e-=2*(o.totalGhosts||0),e-=3*(s.totalClusters||0),e-=1*(r.aiLikelyCount||0);let i=0;if(a&&"object"==typeof a)for(const[,n]of Object.entries(a))n?.issues&&(i+=n.issues.length);return e-=Math.min(.5*i,30),Math.max(0,Math.min(100,Math.round(e)))}_scoreToGrade(n){return n>=90?"A":n>=80?"B":n>=65?"C":n>=50?"D":"F"}_gradeColor(n){return{A:"#00ff88",B:"#88ff00",C:"#ffd93d",D:"#ff8844",F:"#ff4444"}[n]||"#ff4444"}_renderHTML(n){const e=this.generatedAt,t=e.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"}),o=e.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}),s=n.hallResults.phantomAPIs?.length||0,r=n.hallResults.deprecatedAPIs?.length||0,a=n.ghostResults.totalGhosts||0,i=n.ghostResults.totalWastedLines||0,l=n.cloneResults.totalClusters||0,d=n.cloneResults.totalDuplicateLines||0,c=n.aiScoreResults.aiLikelyCount||0,h=n.aiScoreResults.aiPossibleCount||0,m=n.aiScoreResults.aiPercentage||0,g=n.debtCost.totalHoursManual||0,v=n.debtCost.totalCostManual||0,u=n.debtCost.totalHoursThuban||0,p=n.debtCost.totalSaving||0,f=n.debtCost.savingPercent||0,b=n.debtCost.weeklyDebtAccumulation||.7,y=Math.round(4*b*80);let x="";if(n.hallResults.phantomAPIs)for(const e of n.hallResults.phantomAPIs.slice(0,8))x+=`<tr>\n <td class="sev-critical">CRITICAL</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Phantom API")}</td>\n <td>Will crash at runtime — method does not exist</td>\n </tr>`;if(n.hallResults.deprecatedAPIs)for(const e of n.hallResults.deprecatedAPIs.slice(0,5))x+=`<tr>\n <td class="sev-high">HIGH</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Deprecated API")}</td>\n <td>Deprecated — will break in future versions</td>\n </tr>`;let $="";if(n.ghostResults.ghosts)for(const e of n.ghostResults.ghosts.slice(0,8))$+=`<tr>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.wastedLines} lines</td>\n <td>${e.references} references</td>\n </tr>`;let w="";if(n.aiScoreResults.highConfidence)for(const e of n.aiScoreResults.highConfidence.slice(0,8))w+=`<tr>\n <td class="sev-critical">${e.confidence}%</td>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.lineCount} lines</td>\n </tr>`;let C="";if(n.cloneResults.clusters)for(const e of n.cloneResults.clusters.slice(0,6)){const n=e.members.map(n=>`${this._esc(n.file)}:${n.line}`).join("<br>");C+=`<tr>\n <td>${this._esc(e.pattern)}</td>\n <td>${e.totalInstances}</td>\n <td style="font-size:0.75rem">${n}</td>\n <td>${this._esc(e.recommendation)}</td>\n </tr>`}return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Thuban Code Health Report — ${this._esc(this.companyName)}</title>\n<style>\n @page { size: A4; margin: 15mm; }\n @media print {\n body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .no-print { display: none !important; }\n .page-break { page-break-before: always; }\n }\n\n :root {\n --bg: #0a0a1a;\n --surface: #12122a;\n --border: #2a2a5a;\n --text: #e0e0f0;\n --dim: #8888aa;\n --cyan: #00f6ff;\n --green: #00ff88;\n --yellow: #ffd93d;\n --red: #ff4444;\n --mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;\n --sans: 'Segoe UI', system-ui, -apple-system, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 11pt;\n line-height: 1.5;\n max-width: 210mm;\n margin: 0 auto;\n padding: 20mm 15mm;\n }\n\n /* Header */\n .report-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n border-bottom: 2px solid var(--cyan);\n padding-bottom: 1.5rem;\n margin-bottom: 2rem;\n }\n\n .report-header .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .report-header .logo svg { width: 36px; height: 36px; }\n\n .report-header .logo-text {\n font-size: 1.5rem;\n font-weight: 300;\n letter-spacing: 0.3em;\n color: var(--cyan);\n }\n\n .report-header .meta {\n text-align: right;\n font-size: 0.8rem;\n color: var(--dim);\n line-height: 1.8;\n }\n\n .report-title {\n text-align: center;\n margin-bottom: 2.5rem;\n }\n\n .report-title h1 {\n font-size: 1.8rem;\n font-weight: 700;\n margin-bottom: 0.25rem;\n }\n\n .report-title .project-name {\n color: var(--cyan);\n font-size: 1.1rem;\n font-family: var(--mono);\n }\n\n /* Score Ring */\n .score-section {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 3rem;\n margin-bottom: 2.5rem;\n padding: 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n\n .score-ring {\n position: relative;\n width: 140px;\n height: 140px;\n }\n\n .score-ring svg { transform: rotate(-90deg); }\n\n .score-ring .score-value {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .score-ring .grade {\n font-size: 2.5rem;\n font-weight: 700;\n font-family: var(--mono);\n line-height: 1;\n }\n\n .score-ring .number {\n font-size: 0.85rem;\n color: var(--dim);\n }\n\n .score-summary {\n flex: 1;\n }\n\n .score-summary h2 {\n font-size: 1.3rem;\n margin-bottom: 0.75rem;\n }\n\n .stat-row {\n display: flex;\n gap: 2rem;\n flex-wrap: wrap;\n margin-top: 1rem;\n }\n\n .stat {\n text-align: center;\n min-width: 80px;\n }\n\n .stat .stat-value {\n font-size: 1.5rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .stat .stat-label {\n font-size: 0.7rem;\n color: var(--dim);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n .stat-critical .stat-value { color: var(--red); }\n .stat-warning .stat-value { color: var(--yellow); }\n .stat-good .stat-value { color: var(--green); }\n .stat-info .stat-value { color: var(--cyan); }\n\n /* Sections */\n .section {\n margin-bottom: 2rem;\n }\n\n .section-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n }\n\n .section-header h3 {\n font-size: 1rem;\n font-weight: 600;\n }\n\n .section-header .icon { font-size: 1.2rem; }\n\n /* Tables */\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n padding: 0.5rem;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-weight: 600;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n }\n\n td {\n padding: 0.4rem 0.5rem;\n border-bottom: 1px solid rgba(42,42,90,0.3);\n font-family: var(--mono);\n font-size: 0.75rem;\n word-break: break-all;\n }\n\n .sev-critical { color: var(--red); font-weight: 700; }\n .sev-high { color: var(--yellow); font-weight: 600; }\n\n /* Cost Box */\n .cost-box {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 1rem;\n }\n\n .cost-grid {\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 1.5rem;\n text-align: center;\n }\n\n .cost-item .cost-value {\n font-size: 1.8rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .cost-item .cost-label {\n font-size: 0.75rem;\n color: var(--dim);\n margin-top: 0.25rem;\n }\n\n .cost-manual .cost-value { color: var(--red); }\n .cost-thuban .cost-value { color: var(--cyan); }\n .cost-saving .cost-value { color: var(--green); }\n\n .roi-bar {\n margin-top: 1.5rem;\n padding: 1rem;\n background: rgba(0,255,136,0.08);\n border: 1px solid rgba(0,255,136,0.2);\n border-radius: 8px;\n text-align: center;\n font-size: 0.85rem;\n }\n\n .roi-bar strong { color: var(--green); }\n\n /* Warning callout */\n .callout {\n padding: 1rem 1.5rem;\n border-radius: 8px;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .callout-danger {\n background: rgba(255,68,68,0.08);\n border: 1px solid rgba(255,68,68,0.2);\n color: #ffaaaa;\n }\n\n .callout-warning {\n background: rgba(255,217,61,0.08);\n border: 1px solid rgba(255,217,61,0.2);\n color: #ffe088;\n }\n\n /* Footer */\n .report-footer {\n margin-top: 3rem;\n padding-top: 1.5rem;\n border-top: 1px solid var(--border);\n display: flex;\n justify-content: space-between;\n font-size: 0.75rem;\n color: var(--dim);\n }\n\n .report-footer a { color: var(--cyan); text-decoration: none; }\n\n /* Print button */\n .print-btn {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n background: var(--cyan);\n color: var(--bg);\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 8px;\n font-weight: 600;\n font-size: 0.9rem;\n cursor: pointer;\n box-shadow: 0 4px 20px rgba(0,246,255,0.3);\n z-index: 100;\n }\n\n .print-btn:hover { background: #33f8ff; }\n\n /* Responsive for screen viewing */\n @media screen and (max-width: 700px) {\n body { padding: 1rem; }\n .cost-grid { grid-template-columns: 1fr; }\n .stat-row { flex-direction: column; gap: 0.75rem; }\n .score-section { flex-direction: column; gap: 1.5rem; }\n }\n</style>\n</head>\n<body>\n\n<button class="print-btn no-print" onclick="window.print()">Save as PDF</button>\n\n\x3c!-- Header --\x3e\n<div class="report-header">\n <div class="logo">\n <svg viewBox="0 0 32 32" fill="none">\n <polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>\n <polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>\n <circle cx="16" cy="18" r="3" fill="#00f6ff"/>\n </svg>\n <span class="logo-text">THUBAN</span>\n </div>\n <div class="meta">\n Report ID: ${n.reportId}<br>\n Generated: ${t} at ${o}<br>\n Engine: Thuban Code Health v0.3.0\n </div>\n</div>\n\n\x3c!-- Title --\x3e\n<div class="report-title">\n <h1>Code Health Report</h1>\n <div class="project-name">${this._esc(this.companyName)}</div>\n</div>\n\n\x3c!-- Score --\x3e\n<div class="score-section">\n <div class="score-ring">\n <svg width="140" height="140" viewBox="0 0 140 140">\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}22" stroke-width="10"/>\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}" stroke-width="10"\n stroke-dasharray="${Math.round(3.77*n.score)} 377"\n stroke-linecap="round"/>\n </svg>\n <div class="score-value">\n <div class="grade" style="color:${n.gradeColor}">${n.grade}</div>\n <div class="number">${n.score}/100</div>\n </div>\n </div>\n <div class="score-summary">\n <h2>Codebase Health: ${"A"===n.grade?"Excellent":"B"===n.grade?"Good":"C"===n.grade?"Needs Attention":"D"===n.grade?"At Risk":"Critical"}</h2>\n <div class="stat-row">\n <div class="stat stat-info">\n <div class="stat-value">${n.fileCount.toLocaleString()}</div>\n <div class="stat-label">Files Scanned</div>\n </div>\n <div class="stat stat-info">\n <div class="stat-value">${n.lineCount.toLocaleString()}</div>\n <div class="stat-label">Lines of Code</div>\n </div>\n <div class="stat ${s>0?"stat-critical":"stat-good"}">\n <div class="stat-value">${s}</div>\n <div class="stat-label">Hallucinated APIs</div>\n </div>\n <div class="stat ${a>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${a}</div>\n <div class="stat-label">Ghost Functions</div>\n </div>\n <div class="stat ${c>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${m}%</div>\n <div class="stat-label">AI-Generated</div>\n </div>\n </div>\n </div>\n</div>\n\n${s>0?`\n<div class="callout callout-danger">\n <strong>PRODUCTION RISK:</strong> ${s} API call${s>1?"s":""} in this codebase reference${1===s?"s":""} methods that do not exist.\n These will throw runtime errors in production. This is not tech debt — this is code that <strong>will crash</strong>.\n</div>\n`:""}\n\n\x3c!-- Tech Debt Cost --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128176;</span>\n <h3>Technical Debt Cost Analysis</h3>\n </div>\n <div class="cost-box">\n <div class="cost-grid">\n <div class="cost-item cost-manual">\n <div class="cost-value">&pound;${v.toLocaleString()}</div>\n <div class="cost-label">Manual fix cost (${g} dev-hours at &pound;80/hr)</div>\n </div>\n <div class="cost-item cost-thuban">\n <div class="cost-value">${u}h</div>\n <div class="cost-label">Fix time with Thuban auto-fix</div>\n </div>\n <div class="cost-item cost-saving">\n <div class="cost-value">&pound;${p.toLocaleString()}</div>\n <div class="cost-label">You save (${f}%)</div>\n </div>\n </div>\n <div class="roi-bar">\n Every week this debt goes unaddressed, <strong>${b} more hours</strong> accumulate.\n That is <strong>&pound;${y.toLocaleString()}/month</strong> in growing technical debt.\n Thuban Pro costs &pound;19/month. <strong>ROI: ${v>0?Math.round(v/19):0}x.</strong>\n </div>\n </div>\n</div>\n\n${s+r>0?`\n\x3c!-- Hallucinations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128302;</span>\n <h3>AI Hallucination Detection</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n These API calls reference methods, modules, or endpoints that do not exist in the libraries being used.\n They were likely generated by an AI coding tool and will fail at runtime.\n </p>\n <table>\n <thead>\n <tr><th>Severity</th><th>Location</th><th>API Call</th><th>Impact</th></tr>\n </thead>\n <tbody>${x}</tbody>\n </table>\n</div>\n`:""}\n\n${a>0?`\n<div class="page-break"></div>\n\x3c!-- Ghost Code --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128123;</span>\n <h3>Ghost Code — Dead Functions</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${a} function${a>1?"s":""} exist in the codebase but are never called.\n This represents ${i.toLocaleString()} wasted lines of code (${n.ghostResults.wastedPercent||"?"}% of the codebase).\n </p>\n <table>\n <thead>\n <tr><th>Location</th><th>Function</th><th>Size</th><th>References</th></tr>\n </thead>\n <tbody>${$}</tbody>\n </table>\n</div>\n`:""}\n\n${c>0?`\n\x3c!-- AI Confidence --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#129302;</span>\n <h3>AI-Generated Code Confidence</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${m}% of functions show strong signals of AI generation.\n ${c} high confidence, ${h} possible.\n These functions should be reviewed for correctness, edge cases, and test coverage.\n </p>\n <table>\n <thead>\n <tr><th>AI %</th><th>Location</th><th>Function</th><th>Size</th></tr>\n </thead>\n <tbody>${w}</tbody>\n </table>\n</div>\n`:""}\n\n${l>0?`\n\x3c!-- Copy-Paste Drift --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#128203;</span>\n <h3>Copy-Paste Drift</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${l} cluster${l>1?"s":""} of similar code found across ${d} duplicate lines.\n These indicate copy-pasted logic that has drifted apart and should be extracted into shared utilities.\n </p>\n <table>\n <thead>\n <tr><th>Pattern</th><th>Instances</th><th>Locations</th><th>Recommendation</th></tr>\n </thead>\n <tbody>${C}</tbody>\n </table>\n</div>\n`:""}\n\n\x3c!-- Recommendations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">&#9889;</span>\n <h3>Recommended Actions</h3>\n </div>\n <div style="display:grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; font-size:0.85rem;">\n ${s>0?`\n <div style="color:var(--red); font-weight:700;">1. IMMEDIATE</div>\n <div>Fix ${s} hallucinated API${s>1?"s":""} — these will crash in production. Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban fix . --fix</code></div>\n `:""}\n ${a>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"2":"1"}. HIGH</div>\n <div>Remove ${a} ghost function${a>1?"s":""} (${i} wasted lines). Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban ghosts .</code> for full list</div>\n `:""}\n ${l>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"3":"2"}. MEDIUM</div>\n <div>Consolidate ${l} copy-paste cluster${l>1?"s":""} into shared utilities</div>\n `:""}\n ${c>0?`\n <div style="color:var(--cyan); font-weight:700;">${s>0?"4":"3"}. REVIEW</div>\n <div>Audit ${c} high-confidence AI-generated function${c>1?"s":""} for correctness and test coverage</div>\n `:""}\n <div style="color:var(--green); font-weight:700;">ONGOING</div>\n <div>Install pre-commit gate: <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban gate --fix</code> — blocks hallucinated code from entering your repo</div>\n </div>\n</div>\n\n\x3c!-- Footer --\x3e\n<div class="report-footer">\n <div>\n <strong>Thuban</strong> — Code Health Engine<br>\n <a href="https://thuban.dev">thuban.dev</a> &nbsp;|&nbsp; A Silverwings product\n </div>\n <div style="text-align:right;">\n Report: ${n.reportId}<br>\n This report was generated automatically.<br>\n For full details, run: <code style="font-family:var(--mono); color:var(--cyan);">npx thuban scan .</code>\n </div>\n</div>\n\n</body>\n</html>`}_esc(n){return String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")}save(n,e){const t=e||this.rootPath,o=path.resolve(t),s=path.resolve(this.rootPath);if(!o.startsWith(s+path.sep)&&o!==s)throw new Error(`Output directory must be within the project root: ${s}`);const r=`thuban-report-${this.generatedAt.toISOString().split("T")[0]}.html`,a=path.join(o,r);return fs.writeFileSync(a,n,"utf-8"),a}}module.exports=ExecutiveReport;
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),exportVerifier=require("./export-verifier");class HallucinationDetector{constructor(e={}){this.config={rootPath:e.rootPath||process.cwd(),...e},this.builtins=new Set(["assert","async_hooks","buffer","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","domain","events","fs","http","http2","https","inspector","module","net","os","path","perf_hooks","process","punycode","querystring","readline","repl","stream","string_decoder","sys","timers","tls","trace_events","tty","url","util","v8","vm","wasi","worker_threads","zlib","node:fs","node:path","node:os","node:crypto","node:http","node:https","node:events","node:util","node:stream","node:child_process","node:url","node:querystring","node:buffer","node:net","node:tls","node:dns","node:readline","node:zlib","node:worker_threads","node:cluster","node:vm","node:assert","node:timers","node:timers/promises","node:test","fs/promises","stream/promises","timers/promises","dns/promises","readline/promises"]),this.phantomPackages=new Set(["node-fetch-extra","express-async","mongoose-v2","react-native-web-view","axios-retry-enhanced","lodash-extended","moment-timezone-v2","socket.io-extra","graphql-tools-v2","next-auth-v5","prisma-client-v2","tailwind-utils","react-query-v4","jest-extended-v2"]),this.phantomAPIs=[{pattern:/fs\.readFileAsync\s*\(/,suggestion:"Use fs.promises.readFile() or fs.readFileSync()",name:"fs.readFileAsync",id:"HALL_API001"},{pattern:/fs\.writeFileAsync\s*\(/,suggestion:"Use fs.promises.writeFile() or fs.writeFileSync()",name:"fs.writeFileAsync",id:"HALL_API002"},{pattern:/fs\.existsAsync\s*\(/,suggestion:"Use fs.promises.access() or fs.existsSync()",name:"fs.existsAsync",id:"HALL_API003"},{pattern:/path\.exists\s*\(/,suggestion:"Use fs.existsSync() — path module has no exists()",name:"path.exists",id:"HALL_API004"},{pattern:/path\.isFile\s*\(/,suggestion:"Use fs.statSync().isFile() — path module has no isFile()",name:"path.isFile",id:"HALL_API005"},{pattern:/path\.isDirectory\s*\(/,suggestion:"Use fs.statSync().isDirectory()",name:"path.isDirectory",id:"HALL_API006"},{pattern:/console\.success\s*\(/,suggestion:"Use console.log() — console.success() does not exist",name:"console.success",id:"HALL_API007"},{pattern:/console\.verbose\s*\(/,suggestion:"Use console.log() or console.debug()",name:"console.verbose",id:"HALL_API008"},{pattern:/JSON\.tryParse\s*\(/,suggestion:"Use try/catch with JSON.parse()",name:"JSON.tryParse",id:"HALL_API009"},{pattern:/JSON\.safeStringify\s*\(/,suggestion:"Use JSON.stringify() with a replacer",name:"JSON.safeStringify",id:"HALL_API010"},{pattern:/Array\.flatten\s*\(/,suggestion:"Use Array.prototype.flat()",name:"Array.flatten",id:"HALL_API011"},{pattern:/Object\.deepClone\s*\(/,suggestion:"Use structuredClone() or JSON.parse(JSON.stringify())",name:"Object.deepClone",id:"HALL_API012"},{pattern:/Object\.deepMerge\s*\(/,suggestion:"Use a library like lodash.merge or write a custom function",name:"Object.deepMerge",id:"HALL_API013"},{pattern:/String\.prototype\.replaceAll\s*&&.*polyfill/i,suggestion:"replaceAll() is native since ES2021 — no polyfill needed",name:"replaceAll polyfill",id:"HALL_API014"},{pattern:/require\s*\(\s*['"]fs\/sync['"]\s*\)/,suggestion:"fs/sync is not a real module — use fs directly",name:"fs/sync",id:"HALL_API015"},{pattern:/require\s*\(\s*['"]http\/server['"]\s*\)/,suggestion:"http/server is not a real module — use http.createServer()",name:"http/server",id:"HALL_API016"},{pattern:/process\.env\.get\s*\(/,suggestion:"Use process.env.VAR_NAME — process.env is a plain object, not a Map",name:"process.env.get()",id:"HALL_API017"},{pattern:/process\.exit\s*\(\s*['"]/,suggestion:"process.exit() takes a number, not a string",name:"process.exit(string)",id:"HALL_API018"}],this.pythonPhantomAPIs=[{pattern:/os\.path\.exists_sync\s*\(/,suggestion:"Use os.path.exists() — exists_sync does not exist in Python",name:"os.path.exists_sync",id:"PY_HALL001"},{pattern:/json\.tryParse\s*\(/,suggestion:"Use json.loads() with try/except",name:"json.tryParse",id:"PY_HALL002"},{pattern:/\w+\.flatMap\s*\(/,suggestion:"Python lists have no flatMap — use list comprehension or itertools.chain.from_iterable()",name:"list.flatMap",id:"PY_HALL003"},{pattern:/\w+\.merge\s*\((?!.*\bself\b)/,suggestion:"Use {**dict1, **dict2} or dict1 | dict2 (3.9+)",name:"dict.merge",id:"PY_HALL004"},{pattern:/\w+\.format_map\s*\(/,suggestion:"str.format_map exists but is rarely correct — did you mean .format()?",name:"string.format_map misuse",id:"PY_HALL005"},{pattern:/from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i,suggestion:"Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary",name:"Unnecessary OrderedDict",id:"PY_HALL006"},{pattern:/async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i,suggestion:"asyncio.sleep(0) to yield is a code smell — review async design",name:"asyncio.sleep(0) hack",id:"PY_HALL007"},{pattern:/import\s+tensorflow\.v2/,suggestion:"tensorflow.v2 is not a real module — use import tensorflow",name:"tensorflow.v2",id:"PY_HALL008"},{pattern:/from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/,suggestion:"train_test_split_v2 does not exist — use train_test_split",name:"sklearn v2 hallucination",id:"PY_HALL009"},{pattern:/requests\.async_get\s*\(/,suggestion:"requests has no async_get — use aiohttp or httpx",name:"requests.async_get",id:"PY_HALL010"}],this.pythonDeprecated=[{pattern:/from\s+distutils/,suggestion:"distutils removed in Python 3.12 — use setuptools",since:"Python 3.12",name:"distutils",id:"PY_DEPR001"},{pattern:/from\s+imp\s+import/,suggestion:"imp removed in Python 3.12 — use importlib",since:"Python 3.12",name:"imp module",id:"PY_DEPR002"},{pattern:/asyncio\.get_event_loop\(\)/,suggestion:"Deprecated in 3.10+ — use asyncio.run() or get_running_loop()",since:"Python 3.10",name:"asyncio.get_event_loop()",id:"PY_DEPR003"},{pattern:/collections\.MutableMapping/,suggestion:"Moved to collections.abc.MutableMapping in Python 3.3+",since:"Python 3.9",name:"collections.MutableMapping",id:"PY_DEPR004"},{pattern:/optparse\.OptionParser/,suggestion:"optparse deprecated since Python 3.2 — use argparse",since:"Python 3.2",name:"optparse",id:"PY_DEPR005"},{pattern:/cgi\.parse_header\s*\(/,suggestion:"cgi module deprecated in Python 3.11, removed in 3.13",since:"Python 3.11",name:"cgi module",id:"PY_DEPR006"},{pattern:/from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/,suggestion:"Use built-in list, dict, tuple, set for type hints (Python 3.9+)",since:"Python 3.9",name:"typing.List/Dict/Tuple",id:"PY_DEPR007"},{pattern:/unittest\.makeSuite\s*\(/,suggestion:"makeSuite deprecated — use TestLoader.loadTestsFromTestCase",since:"Python 3.11",name:"unittest.makeSuite",id:"PY_DEPR008"}],this.pythonSmells=[{pattern:/print\s*\(\s*f?['"]\s*debug/i,id:"PY_SMELL001",name:"Debug Print",message:"Debug print statement left in code"},{pattern:/except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m,id:"PY_SMELL002",name:"Bare Except",message:"Bare except or swallowed exception — hides real errors"},{pattern:/#\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PY_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError\s*\(?\s*\)?/,id:"PY_SMELL004",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i,id:"PY_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PY_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/import\s+\*/,id:"PY_SMELL007",name:"Wildcard Import",message:"Wildcard import — pollutes namespace, hides dependencies"}],this.goDeprecated=[{pattern:/ioutil\.ReadFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.ReadFile()",since:"Go 1.16",name:"ioutil.ReadFile",id:"GO_DEPR001"},{pattern:/ioutil\.WriteFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.WriteFile()",since:"Go 1.16",name:"ioutil.WriteFile",id:"GO_DEPR002"},{pattern:/ioutil\.TempDir\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.MkdirTemp()",since:"Go 1.16",name:"ioutil.TempDir",id:"GO_DEPR003"},{pattern:/ioutil\.ReadAll\s*\(/,suggestion:"ioutil removed in Go 1.16 — use io.ReadAll()",since:"Go 1.16",name:"ioutil.ReadAll",id:"GO_DEPR004"},{pattern:/strings\.Title\s*\(/,suggestion:"strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text",since:"Go 1.18",name:"strings.Title",id:"GO_DEPR005"}],this.goSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"GO_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/panic\s*\(\s*["']not implemented["']\s*\)/,id:"GO_SMELL002",name:"Panic Not Implemented",message:"Stub implementation — will panic at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"GO_SMELL003",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.rustPhantomAPIs=[{pattern:/std::fs::File::open_async\s*\(/,suggestion:"Use tokio::fs::File::open() for async file I/O",name:"File::open_async",id:"RS_HALL001"},{pattern:/Vec::flatten\s*\(/,suggestion:"Use .into_iter().flatten() — Vec has no flatten() method",name:"Vec::flatten",id:"RS_HALL002"},{pattern:/HashMap::get_or_default\s*\(/,suggestion:"Use .get().unwrap_or(&default) or entry().or_insert()",name:"HashMap::get_or_default",id:"RS_HALL003"},{pattern:/String::from_utf8_lossy_owned\s*\(/,suggestion:"Use String::from_utf8_lossy().into_owned()",name:"String::from_utf8_lossy_owned",id:"RS_HALL004"},{pattern:/\.async_iter\s*\(/,suggestion:"Use futures::stream::iter() from the futures crate",name:".async_iter()",id:"RS_HALL005"},{pattern:/std::net::TcpStream::connect_async\s*\(/,suggestion:"Use tokio::net::TcpStream::connect()",name:"TcpStream::connect_async",id:"RS_HALL006"},{pattern:/std::thread::sleep_async\s*\(/,suggestion:"Use tokio::time::sleep() for async sleep",name:"thread::sleep_async",id:"RS_HALL007"},{pattern:/Vec::remove_all\s*\(/,suggestion:"Use .retain(|x| condition) or .clear()",name:"Vec::remove_all",id:"RS_HALL008"},{pattern:/str::split_whitespace_n\s*\(/,suggestion:"Use .splitn(n, char::is_whitespace) instead",name:"str::split_whitespace_n",id:"RS_HALL009"},{pattern:/\.map_async\s*\(/,suggestion:"Use futures::future::join_all() or tokio::task::spawn",name:".map_async()",id:"RS_HALL010"}],this.rustDeprecated=[{pattern:/std::sync::ONCE_INIT/,suggestion:"std::sync::ONCE_INIT removed — use Once::new()",since:"Rust 1.38",name:"ONCE_INIT",id:"RS_DEPR001"},{pattern:/\bstd::mem::uninitialized\s*\(/,suggestion:"std::mem::uninitialized() removed — use MaybeUninit::uninit()",since:"Rust 1.39",name:"mem::uninitialized",id:"RS_DEPR002"},{pattern:/\btry!\s*\(/,suggestion:"try!() macro removed — use the ? operator",since:"Rust 2018",name:"try!() macro",id:"RS_DEPR003"},{pattern:/extern\s+crate\s+std\s*;/,suggestion:"extern crate std is implicit since Rust 2018 edition",since:"Rust 2018",name:"extern crate std",id:"RS_DEPR004"},{pattern:/extern\s+crate\s+alloc\s*;/,suggestion:"extern crate alloc is implicit in 2018+ edition",since:"Rust 2018",name:"extern crate alloc",id:"RS_DEPR005"},{pattern:/std::error::Error::cause\s*\(/,suggestion:".cause() deprecated — use .source() instead",since:"Rust 1.33",name:"Error::cause()",id:"RS_DEPR006"}],this.rustSmells=[{pattern:/todo!\s*\(\s*\)/,id:"RS_SMELL001",name:"todo! macro",message:"todo!() macro — will panic at runtime"},{pattern:/unimplemented!\s*\(\s*\)/,id:"RS_SMELL002",name:"unimplemented! macro",message:"unimplemented!() macro — will panic at runtime"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RS_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/unwrap\(\).*unwrap\(\)/,id:"RS_SMELL004",name:"Double Unwrap",message:"Chained unwrap() — will panic on None/Err"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"RS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/unsafe\s*\{/,id:"RS_SMELL006",name:"Unsafe Block",message:"unsafe block — verify memory safety guarantees"},{pattern:/\.unwrap\(\)\s*;/,id:"RS_SMELL007",name:"Bare unwrap()",message:".unwrap() will panic on None/Err — use ? or match"},{pattern:/println!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL008",name:"Debug println!",message:"Debug println! left in code — use the log crate"},{pattern:/eprintln!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL009",name:"Debug eprintln!",message:"Debug eprintln! left in code — use the log crate"},{pattern:/#\[allow\(dead_code\)\]/,id:"RS_SMELL010",name:"Suppressed Dead Code",message:"#[allow(dead_code)] suppresses warnings — remove unused code"}],this.javaSmells=[{pattern:/System\.out\.println\s*\(.*debug/i,id:"JAVA_SMELL001",name:"Debug Println",message:"Debug System.out.println — use a logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"JAVA_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i,id:"JAVA_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"JAVA_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"JAVA_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.csharpSmells=[{pattern:/Console\.WriteLine\s*\(.*[Dd]ebug/i,id:"CS_SMELL001",name:"Debug WriteLine",message:"Debug Console.WriteLine — use ILogger or Debug.Write"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"CS_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:NotImplementedException)\s*\(/,id:"CS_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"CS_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"CS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.kotlinPhantomAPIs=[{pattern:/\.flatMap\s*\{[^}]*\}(?!.*asSequence)/,suggestion:"Use .flatMap { } — verify this is not confusing sequence vs list behavior",name:"flatMap confusion",id:"KT_HALL001"},{pattern:/listOf\(\)\.stream\s*\(/,suggestion:"Kotlin lists have no .stream() — use .asSequence() or direct collection ops",name:"listOf().stream()",id:"KT_HALL002"},{pattern:/String\.format\s*\(/,suggestion:'Prefer Kotlin string templates "$variable" over String.format()',name:"String.format Java-style",id:"KT_HALL003"},{pattern:/\bObject\s*\(\s*\)/,suggestion:"Kotlin uses Any() not Object() — or use object keyword for singletons",name:"Object() Java-style",id:"KT_HALL004"},{pattern:/\.forEach\s*\(\s*::println\s*\)(?!.*debug)/i,suggestion:"println is for debug output — use a logger",name:"println via forEach",id:"KT_HALL005"},{pattern:/coroutineScope\s*\{\s*launch\s*\{[^}]*\}\s*\}(?!\s*\.join)/,suggestion:"Unjoined coroutine in coroutineScope may cause issues — ensure structured concurrency",name:"Unawaited launch",id:"KT_HALL006"},{pattern:/Dispatchers\.IO\s*\+\s*Dispatchers/,suggestion:"Combining Dispatchers is not valid — use one dispatcher at a time",name:"Combined Dispatchers",id:"KT_HALL007"},{pattern:/\.toList\(\)\.stream\(\)/,suggestion:"Use Kotlin sequences (.asSequence()) instead of .toList().stream()",name:"toList().stream()",id:"KT_HALL008"},{pattern:/companion object.*getInstance/i,suggestion:"Kotlin idiom for singletons is object keyword, not companion object + getInstance()",name:"Java-style singleton",id:"KT_HALL009"},{pattern:/lateinit var.*\?/,suggestion:"lateinit properties cannot be nullable — remove ? or use by lazy {}",name:"lateinit nullable",id:"KT_HALL010"}],this.kotlinDeprecated=[{pattern:/\bapply\s+plugin:\s*['"]kotlin-android-extensions['"]/,suggestion:"kotlin-android-extensions deprecated — use view binding or Jetpack ViewBinding",since:"Kotlin 1.8",name:"kotlin-android-extensions",id:"KT_DEPR001"},{pattern:/\bkotlinx\.android\.synthetic/,suggestion:"Synthetic imports deprecated — use view binding or findViewById",since:"Kotlin 1.8",name:"Synthetic imports",id:"KT_DEPR002"},{pattern:/\bCoroutineScope\(EmptyCoroutineContext\)/,suggestion:"EmptyCoroutineContext coroutine scope is error-prone — use viewModelScope or lifecycleScope",since:"Kotlin Coroutines 1.6",name:"EmptyCoroutineContext scope",id:"KT_DEPR003"},{pattern:/\bBuildersKt\.launch\b/,suggestion:"Internal BuildersKt APIs are deprecated — use coroutineScope { launch {} }",since:"Kotlin Coroutines 1.5",name:"BuildersKt.launch",id:"KT_DEPR004"},{pattern:/\basyncLazy\s*\{/,suggestion:"asyncLazy {} is not a standard Kotlin API — use lazy {} or async {}",since:"Kotlin 1.6",name:"asyncLazy",id:"KT_DEPR005"}],this.kotlinSmells=[{pattern:/\bprintln\s*\(/,id:"KT_SMELL001",name:"Debug println",message:"println() is for debug output — use a proper logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"KT_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/TODO\s*\(\s*\)/,id:"KT_SMELL003",name:"TODO() stub",message:"TODO() will throw NotImplementedError at runtime"},{pattern:/!!\s*\./,id:"KT_SMELL004",name:"Non-null assertion chain",message:"!! non-null assertion — will throw NullPointerException if null"},{pattern:/\bas\s+\w+\b(?!.*\?)/,suggestion:"Unsafe cast — prefer safe cast (as? Type) with null check",id:"KT_SMELL005",name:"Unsafe cast",message:"Unsafe cast with as — will throw ClassCastException if wrong type"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"KT_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/catch\s*\(\s*e:\s*Exception\s*\)\s*\{\s*\}/,id:"KT_SMELL007",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/throw\s+NotImplementedError\s*\(/,id:"KT_SMELL008",name:"Not Implemented",message:"Stub implementation — will throw at runtime"}],this.phpPhantomAPIs=[{pattern:/array_flatten\s*\(/,suggestion:"PHP has no array_flatten() — use array_merge(...$array) or a recursive function",name:"array_flatten",id:"PHP_HALL001"},{pattern:/str_contains_all\s*\(/,suggestion:"PHP has no str_contains_all() — use multiple str_contains() calls",name:"str_contains_all",id:"PHP_HALL002"},{pattern:/array_unique_values\s*\(/,suggestion:"PHP has no array_unique_values() — use array_values(array_unique())",name:"array_unique_values",id:"PHP_HALL003"},{pattern:/\$pdo->fetchAll\s*\(/,suggestion:"fetchAll() is on PDOStatement not PDO — call $stmt->fetchAll()",name:"PDO::fetchAll",id:"PHP_HALL004"},{pattern:/json_decode_safe\s*\(/,suggestion:"PHP has no json_decode_safe() — wrap json_decode() with json_last_error() check",name:"json_decode_safe",id:"PHP_HALL005"},{pattern:/str_replace_all\s*\(/,suggestion:"PHP has no str_replace_all() — use str_replace() which already replaces all occurrences",name:"str_replace_all",id:"PHP_HALL006"},{pattern:/array_map_keys\s*\(/,suggestion:"PHP has no array_map_keys() — use array_combine(array_map(...), array_keys())",name:"array_map_keys",id:"PHP_HALL007"},{pattern:/\$request->getJson\s*\(/,suggestion:'Not a standard PHP function — use json_decode(file_get_contents("php://input"))',name:"$request->getJson",id:"PHP_HALL008"},{pattern:/Date::now\s*\(/,suggestion:"PHP has no Date::now() — use new DateTime() or time()",name:"Date::now",id:"PHP_HALL009"},{pattern:/\bawait\s+/,suggestion:"PHP has no await keyword — use synchronous code or ReactPHP for async",name:"await keyword",id:"PHP_HALL010"}],this.phpDeprecated=[{pattern:/\bmysql_connect\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_connect",id:"PHP_DEPR001"},{pattern:/\bmysql_query\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_query",id:"PHP_DEPR002"},{pattern:/\bmysql_fetch_array\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_fetch_array",id:"PHP_DEPR003"},{pattern:/\bereg\s*\(/,suggestion:"ereg() removed in PHP 7 — use preg_match()",since:"PHP 7.0",name:"ereg()",id:"PHP_DEPR004"},{pattern:/\bsplit\s*\(/,suggestion:"split() removed in PHP 7 — use preg_split() or explode()",since:"PHP 7.0",name:"split()",id:"PHP_DEPR005"},{pattern:/\bcreate_function\s*\(/,suggestion:"create_function() removed in PHP 8 — use anonymous functions (closures)",since:"PHP 8.0",name:"create_function",id:"PHP_DEPR006"},{pattern:/\bmagic_quotes_gpc\b/,suggestion:"magic_quotes_gpc removed in PHP 7 — sanitize inputs manually",since:"PHP 7.0",name:"magic_quotes_gpc",id:"PHP_DEPR007"},{pattern:/\beach\s*\(/,suggestion:"each() deprecated in PHP 7.2, removed in PHP 8 — use foreach",since:"PHP 8.0",name:"each()",id:"PHP_DEPR008"},{pattern:/\bmcrypt_/,suggestion:"mcrypt removed in PHP 7.2 — use OpenSSL functions",since:"PHP 7.2",name:"mcrypt functions",id:"PHP_DEPR009"}],this.phpSmells=[{pattern:/\beval\s*\(/,id:"PHP_SMELL001",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bextract\s*\(\s*\$_(?:GET|POST|REQUEST)/,id:"PHP_SMELL002",name:"extract() from superglobal",message:"extract($_GET/_POST) — variable injection vulnerability"},{pattern:/\$\$\w+/,id:"PHP_SMELL003",name:"Variable variable",message:"Variable variables ($$var) — hard to trace and injection risk"},{pattern:/\bshell_exec\s*\(/,id:"PHP_SMELL004",name:"shell_exec()",message:"shell_exec() — remote code execution risk if input unsanitized"},{pattern:/\bsystem\s*\(/,id:"PHP_SMELL005",name:"system()",message:"system() — remote code execution risk if input unsanitized"},{pattern:/\bpassthru\s*\(/,id:"PHP_SMELL006",name:"passthru()",message:"passthru() — remote code execution risk if input unsanitized"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PHP_SMELL007",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PHP_SMELL008",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/\$_(?:GET|POST|REQUEST)\[.*\].*SELECT.*\./i,id:"PHP_SMELL009",name:"SQL Injection Risk",message:"User input directly in SQL string — use prepared statements"},{pattern:/\bvar_dump\s*\(/,id:"PHP_SMELL010",name:"Debug var_dump",message:"var_dump() is for debug only — remove before production"}],this.rubyPhantomAPIs=[{pattern:/\.flatten_map\s*\{/,suggestion:"Ruby has no .flatten_map — use .flat_map { }",name:".flatten_map",id:"RB_HALL001"},{pattern:/Array\.of\s*\(/,suggestion:"Ruby has no Array.of() — use Array() or []",name:"Array.of()",id:"RB_HALL002"},{pattern:/Hash\.from_array\s*\(/,suggestion:"Ruby has no Hash.from_array() — use array.to_h or Hash[array]",name:"Hash.from_array",id:"RB_HALL003"},{pattern:/\.async\s*\{/,suggestion:"Ruby has no .async {} — use Thread.new or concurrent-ruby gem",name:".async block",id:"RB_HALL004"},{pattern:/String\.format\s*\(/,suggestion:"Ruby has no String.format() — use string interpolation or % operator",name:"String.format",id:"RB_HALL005"},{pattern:/\bpromise\s*\{/,suggestion:"Ruby has no built-in promise {} — use concurrent-ruby gem or Thread",name:"promise block",id:"RB_HALL006"},{pattern:/\.try!\s*\(/,suggestion:"Ruby has no .try!() — use &. (safe navigation) or Rails .try()",name:".try!()",id:"RB_HALL007"},{pattern:/JSON\.safe_parse\s*\(/,suggestion:"Ruby has no JSON.safe_parse — use JSON.parse with rescue",name:"JSON.safe_parse",id:"RB_HALL008"},{pattern:/ActiveRecord::Base\.execute\s*\(/,suggestion:"ActiveRecord::Base has no .execute() — use connection.execute() or where()",name:"Base.execute",id:"RB_HALL009"},{pattern:/\.collect_map\s*\{/,suggestion:"Ruby has no .collect_map — use .map { }.flatten(1) or .flat_map",name:".collect_map",id:"RB_HALL010"}],this.rubyDeprecated=[{pattern:/\brequire\s+['"]thread['"]/,suggestion:'require "thread" is deprecated — Thread is now built-in without require',since:"Ruby 2.0",name:'require "thread"',id:"RB_DEPR001"},{pattern:/\bObject#type\b|\.type\s*==/,suggestion:".type is deprecated — use .class or .is_a?",since:"Ruby 1.8",name:"Object#type",id:"RB_DEPR002"},{pattern:/\bERB::Util\.html_escape\b/,suggestion:"ERB::Util.html_escape deprecated — use CGI.escapeHTML or h() in views",since:"Ruby 2.6",name:"ERB::Util.html_escape",id:"RB_DEPR003"},{pattern:/\bFile\.exists\?\s*\(/,suggestion:"File.exists? deprecated — use File.exist? (no s)",since:"Ruby 2.2",name:"File.exists?",id:"RB_DEPR004"},{pattern:/\bDir\.exists\?\s*\(/,suggestion:"Dir.exists? deprecated — use Dir.exist? (no s)",since:"Ruby 2.2",name:"Dir.exists?",id:"RB_DEPR005"},{pattern:/\bObject#returning\b|\s+returning\s+/,suggestion:"Object#returning removed from Rails core — use tap { |obj| }",since:"Rails 3.0",name:"Object#returning",id:"RB_DEPR006"}],this.rubySmells=[{pattern:/\bputs\s+(?!STDOUT)/,id:"RB_SMELL001",name:"Debug puts",message:"puts is for debug output — use Rails logger or a logging library"},{pattern:/\bp\s+\w/,id:"RB_SMELL002",name:"Debug p()",message:"p() is for debug output — remove before production"},{pattern:/\beval\s*\(/,id:"RB_SMELL003",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bsystem\s*\(/,id:"RB_SMELL004",name:"system() Shell Call",message:"system() call — command injection risk if input unsanitized"},{pattern:/`[^`]+`/,id:"RB_SMELL005",name:"Backtick Shell Exec",message:"Backtick shell execution — command injection risk"},{pattern:/\bexec\s*\(/,id:"RB_SMELL006",name:"exec() call",message:"exec() replaces the process — ensure this is intentional"},{pattern:/\brescue\s*$|\brescue\s+Exception\b/,id:"RB_SMELL007",name:"Bare rescue",message:"Bare rescue catches all exceptions including fatal ones — be specific"},{pattern:/["'].*#\{.*sql.*\}.*["']/i,id:"RB_SMELL008",name:"SQL String Interpolation",message:"SQL built with string interpolation — use parameterized queries"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RB_SMELL009",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError/,id:"RB_SMELL010",name:"Not Implemented",message:"Stub implementation — will raise at runtime"}],this.jsExtensions=new Set([".js",".ts",".jsx",".tsx",".mjs",".cjs"]),this.pyExtensions=new Set([".py",".pyw"]),this.goExtensions=new Set([".go"]),this.rustExtensions=new Set([".rs"]),this.javaExtensions=new Set([".java",".kt"]),this.csharpExtensions=new Set([".cs"]),this.phpExtensions=new Set([".php"]),this.rubyExtensions=new Set([".rb"]),this.aiCodeSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i,id:"AI_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished implementation"},{pattern:/throw new Error\(['"]Not implemented['"]\)/,id:"AI_SMELL002",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/\/\/\s*\.\.\.\s*(rest|remaining|more|other|additional)/i,id:"AI_SMELL003",name:"Truncated Code",message:"AI output was truncated — code is incomplete"},{pattern:/\/\/\s*(your|replace|insert|put)\s+(code|logic|implementation|api[_\s]?key)/i,id:"AI_SMELL004",name:"Template Placeholder",message:"Template placeholder left in code — needs real implementation"},{pattern:/['"]your-api-key-here['"]|['"]sk-[.]{3,}['"]|['"]xxx+['"]/i,id:"AI_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/example\.com|test@test\.com|john@doe\.com|foo@bar\.com/i,id:"AI_SMELL006",name:"Example Domain",message:"Example domain/email in production code"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"AI_SMELL007",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.deprecatedAPIs=[{pattern:/new Buffer\s*\(/,suggestion:"Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()",since:"Node 6",name:"new Buffer()",id:"DEPR_API001"},{pattern:/require\s*\(\s*['"]sys['"]\s*\)/,suggestion:'Use require("util") — sys was removed in Node 1.0',since:"Node 1.0",name:'require("sys")',id:"DEPR_API002"},{pattern:/fs\.exists\s*\((?!Sync)/,suggestion:"Use fs.access() or fs.stat() — fs.exists() is deprecated",since:"Node 1.0",name:"fs.exists()",id:"DEPR_API003"},{pattern:/domain\.create\s*\(/,suggestion:"Use async_hooks or try/catch — domain module is deprecated",since:"Node 4",name:"domain.create()",id:"DEPR_API004",fixable:!1},{pattern:/require\s*\(\s*['"]punycode['"]\s*\)/,suggestion:"Use userland punycode package — built-in is deprecated",since:"Node 7",name:'require("punycode")',id:"DEPR_API005",fixable:!1},{pattern:/url\.parse\s*\(/,suggestion:"Use new URL() — url.parse() is legacy",since:"Node 11",name:"url.parse()",id:"DEPR_API006"},{pattern:/querystring\.(parse|stringify)\s*\(/,suggestion:"Use URLSearchParams — querystring is legacy",since:"Node 14",name:"querystring",id:"DEPR_API007",fixable:!1},{pattern:/util\.isArray\s*\(/,suggestion:"Use Array.isArray() — util type checks are deprecated",since:"Node 4",name:"util.isArray()",id:"DEPR_API008"},{pattern:/util\.isFunction\s*\(/,suggestion:'Use typeof fn === "function"',since:"Node 4",name:"util.isFunction()",id:"DEPR_API009"},{pattern:/util\.pump\s*\(/,suggestion:"Use stream.pipeline() — util.pump() was removed",since:"Node 1.0",name:"util.pump()",id:"DEPR_API010",fixable:!1}]}async scan(e){const t=Date.now(),s={phantomImports:[],phantomAPIs:[],aiSmells:[],deprecatedAPIs:[],unresolvedDeps:[],stats:{filesScanned:0,totalIssues:0,scanTime:0}},n=this._loadDeclaredDeps(),i=this._loadIgnoreRules();for(const t of e)try{const e=path.extname(t).toLowerCase(),a=this.jsExtensions.has(e),o=this.pyExtensions.has(e),r=this.goExtensions.has(e),l=this.rustExtensions.has(e),c=this.javaExtensions.has(e),d=this.csharpExtensions.has(e),p=this.phpExtensions.has(e),m=this.rubyExtensions.has(e);if(!(a||o||r||l||c||d||p||m))continue;const u=path.relative(this.config.rootPath,t);if(i.some(e=>u.includes(e)||t.includes(e)))continue;const g=fs.readFileSync(t,"utf-8"),h=g.split("\n"),_=new Set;for(let e=0;e<h.length;e++)h[e].includes("thuban-ignore")&&(_.add(e+1),_.add(e+2));const f=this._isPatternDefinitionFile(g);s.stats.filesScanned++,a?(this._checkPhantomImports(u,g,h,n,s,_),f||this._checkPhantomAPIs(u,g,h,s,_),this._checkAISmells(u,g,h,s,_),f||this._checkDeprecatedAPIs(u,g,h,s,_),f||this._checkNamedExports(u,t,g,s,_)):o?(this._checkLanguagePatterns(u,g,h,s,_,this.pythonPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.pythonDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.pythonSmells)):p?(this._checkLanguagePatterns(u,g,h,s,_,this.phpPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.phpDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.phpSmells)):m?(this._checkLanguagePatterns(u,g,h,s,_,this.rubyPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rubyDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rubySmells)):r?(this._checkLanguagePatterns(u,g,h,s,_,this.goDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.goSmells)):l?(this._checkLanguagePatterns(u,g,h,s,_,this.rustPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rustDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rustSmells)):c?".kt"===e?(this._checkLanguagePatterns(u,g,h,s,_,this.kotlinPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.kotlinDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.kotlinSmells)):this._checkLanguageSmells(u,g,h,s,_,this.javaSmells):d&&this._checkLanguageSmells(u,g,h,s,_,this.csharpSmells)}catch(e){}return s.stats.totalIssues=s.phantomImports.length+s.phantomAPIs.length+s.aiSmells.length+s.deprecatedAPIs.length+s.unresolvedDeps.length,s.stats.scanTime=Date.now()-t,s}_safeRegexTest(e,t,s=1e3){const n=Date.now();try{const i=e.test(t),a=Date.now()-n;return a>s?(console.warn(`[HALLUCINATION-DETECTOR] WARNING: Regex ${e.source.substring(0,50)}... took ${a}ms on input (${t.length} chars) — skipping pattern`),null):i}catch(e){return console.warn("[HALLUCINATION-DETECTOR] Regex error during scan"),null}}_safeRegexExec(e,t,s=1e3){const n=Date.now();try{const i=e.exec(t),a=Date.now()-n;return a>s?(console.warn(`[HALLUCINATION-DETECTOR] WARNING: Regex exec ${e.source.substring(0,50)}... took ${a}ms — skipping pattern`),null):i}catch(e){return console.warn("[HALLUCINATION-DETECTOR] Regex exec error during scan"),null}}_checkLanguagePatterns(e,t,s,n,i,a,o){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const r=s[t];for(const s of a){const i=this._safeRegexTest(s.pattern,r);if(null!==i&&i){const i={file:e,line:t+1,name:s.name,id:s.id};s.suggestion&&(i.suggestion=s.suggestion),s.since&&(i.since=s.since,i.fix=s.suggestion),"deprecatedAPIs"===o?n.deprecatedAPIs.push(i):n.phantomAPIs.push(i)}}}}_checkLanguageSmells(e,t,s,n,i,a){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const o=s[t];for(const s of a){const i=this._safeRegexTest(s.pattern,o);null!==i&&i&&n.aiSmells.push({file:e,line:t+1,id:s.id,name:s.name,message:s.message,type:"ai_smell",severity:"medium"})}}}formatReport(e){const t="",s="",n="",i="",a="",o="",r="",l="";let c="";if(c+=`\n${l} ╔══════════════════════════════════════════════╗${t}\n`,c+=`${l} ║${s} THUBAN HALLUCINATION REPORT ${t}${l}║${t}\n`,c+=`${l} ╚══════════════════════════════════════════════╝${t}\n\n`,0===e.stats.totalIssues)return c+=` ${i}${s}No hallucinations detected.${t} ${r}Clean codebase.${t}\n\n`,c;if(e.phantomImports.length>0){c+=` ${n}${s}Phantom Imports (${e.phantomImports.length})${t}\n`,c+=` ${r}Packages that don't exist in package.json or node_modules${t}\n\n`;for(const s of e.phantomImports.slice(0,10))c+=` ${n}✗${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.module}${t} ${r}— ${s.reason}${t}\n`;e.phantomImports.length>10&&(c+=` ${r}... and ${e.phantomImports.length-10} more${t}\n`),c+="\n"}if(e.phantomAPIs.length>0){c+=` ${n}${s}Phantom APIs (${e.phantomAPIs.length})${t}\n`,c+=` ${r}Methods/properties that don't exist on their objects${t}\n\n`;for(const s of e.phantomAPIs.slice(0,10))c+=` ${n}✗${t} ${s.id?`${r}[${s.id}]${t} `:""}${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}— ${s.suggestion}${t}\n`,c+=` ${r}Suppress: // thuban-ignore ${s.id||"HALL_API"}${t}\n`;e.phantomAPIs.length>10&&(c+=` ${r}... and ${e.phantomAPIs.length-10} more${t}\n`),c+="\n"}if(e.aiSmells.length>0){c+=` ${a}${s}AI Code Smells (${e.aiSmells.length})${t}\n`,c+=` ${r}Patterns typical of AI-generated code that needs review${t}\n\n`;for(const s of e.aiSmells.slice(0,10))c+=` ${a}!${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${r}${s.message}${t}\n`;e.aiSmells.length>10&&(c+=` ${r}... and ${e.aiSmells.length-10} more${t}\n`),c+="\n"}if(e.deprecatedAPIs.length>0){c+=` ${a}${s}Deprecated APIs (${e.deprecatedAPIs.length})${t}\n`,c+=` ${r}APIs that LLMs still suggest but are deprecated or removed${t}\n\n`;for(const s of e.deprecatedAPIs.slice(0,10))c+=` ${a}⚠${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}(deprecated since ${s.since})${t}\n`,c+=` ${i}→${t} ${r}${s.suggestion}${t}\n`;e.deprecatedAPIs.length>10&&(c+=` ${r}... and ${e.deprecatedAPIs.length-10} more${t}\n`),c+="\n"}return c+=` ${s}Summary:${t} ${o}${e.stats.filesScanned}${t} files scanned, `,c+=`${e.stats.totalIssues>0?n:i}${e.stats.totalIssues}${t} hallucination issues found `,c+=`${r}(${e.stats.scanTime}ms)${t}\n\n`,c}_loadDeclaredDeps(){const e=new Set;return this._packageJsonPaths=new Map,this._collectPackageJsonDeps(this.config.rootPath,e,0),e}_collectPackageJsonDeps(e,t,s){if(!(s>5))try{const n=path.join(e,"package.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf-8")),i=new Set;for(const e of Object.keys(s.dependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.devDependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.peerDependencies||{}))t.add(e),i.add(e);this._packageJsonPaths.set(e,i)}const i=new Set(["node_modules",".git","dist","build","coverage",".next",".cache","vendor"]),a=fs.readdirSync(e,{withFileTypes:!0});for(const n of a)!n.isDirectory()||i.has(n.name)||n.name.startsWith(".")||this._collectPackageJsonDeps(path.join(e,n.name),t,s+1)}catch(e){}}_checkPhantomImports(e,t,s,n,i,a=new Set){const o=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const s of o){let o;for(;null!==(o=s.exec(t));){const s=o[1];if(s.startsWith(".")||s.startsWith("/"))continue;const r=s.split("/")[0];if(this.builtins.has(s)||this.builtins.has(r))continue;const l=s.startsWith("@")?s.split("/").slice(0,2).join("/"):r;if(!n.has(l)&&!this._findInNodeModules(file,l)){const n=this._findLineNumber(t,o.index);if(a.has(n))continue;const r=this.phantomPackages.has(s);i.phantomImports.push({file:e,line:n,module:s,reason:r?"Known AI-hallucinated package — does not exist on npm":"Not in package.json and not in node_modules",severity:r?"critical":"high",fixable:!1})}}}}_checkNamedExports(e,t,s,n,i=new Set){let a;try{a=exportVerifier.verifyFile(t,s)}catch(e){return}for(const t of a)i.has(t.line)||n.phantomImports.push({file:e,line:t.line,module:t.source,reason:t.message,severity:"critical",fixable:!1,source:"export-verifier"})}_checkPhantomAPIs(e,t,s,n,i=new Set){for(const a of this.phantomAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.phantomAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,severity:"high",fixable:!0,fixAction:"replace_phantom_api"})}}}_checkAISmells(e,t,s,n,i=new Set){for(const a of this.aiCodeSmells){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);i.has(r)||n.aiSmells.push({file:e,line:r,id:a.id,name:a.name,message:a.message,severity:"warning",code:s[r-1]?.trim().substring(0,100)})}}}_checkDeprecatedAPIs(e,t,s,n,i=new Set){for(const a of this.deprecatedAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.deprecatedAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,since:a.since,severity:"warning",fixable:!0,fixAction:"update_deprecated_api"})}}}_findLineNumber(e,t){let s=1;for(let n=0;n<t&&n<e.length;n++)"\n"===e[n]&&s++;return s}_isPatternDefinitionFile(e){const t=(e.match(/pattern:\s*\//g)||[]).length,s=(e.match(/phantom|hallucin/gi)||[]).length;return t>5&&s>2}_isInsidePattern(e,t){const s=e.trim();return!!(/pattern:\s*\//.test(s)||s.startsWith("//")||s.startsWith("*")||/(?:name|suggestion|message|description):\s*['"]/.test(s))}_loadIgnoreRules(){const e=[];try{const t=path.join(this.config.rootPath,".thubanrc.json"),s=JSON.parse(fs.readFileSync(t,"utf-8"));s.hallucination?.ignore&&e.push(...s.hallucination.ignore),s.ignore&&e.push(...s.ignore)}catch(e){}return e}_findInNodeModules(e,t){let s=path.dirname(e);const n=this.config.rootPath;for(;s.length>=n.length;){const e=path.join(s,"node_modules",t);try{if(fs.existsSync(e))return!0}catch(e){}const n=path.dirname(s);if(n===s)break;s=n}return!1}}module.exports=HallucinationDetector;
1
+ const fs=require("fs"),path=require("path"),exportVerifier=require("./export-verifier");class HallucinationDetector{constructor(e={}){const t=new Set(["rootPath","ignorePatterns","maxFileSize"]),s={};for(const n of Object.keys(e))t.has(n)&&(s[n]=e[n]);this.config={rootPath:s.rootPath||process.cwd()},this.builtins=new Set(["assert","async_hooks","buffer","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","domain","events","fs","http","http2","https","inspector","module","net","os","path","perf_hooks","process","punycode","querystring","readline","repl","stream","string_decoder","sys","timers","tls","trace_events","tty","url","util","v8","vm","wasi","worker_threads","zlib","node:fs","node:path","node:os","node:crypto","node:http","node:https","node:events","node:util","node:stream","node:child_process","node:url","node:querystring","node:buffer","node:net","node:tls","node:dns","node:readline","node:zlib","node:worker_threads","node:cluster","node:vm","node:assert","node:timers","node:timers/promises","node:test","fs/promises","stream/promises","timers/promises","dns/promises","readline/promises"]),this.phantomPackages=new Set(["node-fetch-extra","express-async","mongoose-v2","react-native-web-view","axios-retry-enhanced","lodash-extended","moment-timezone-v2","socket.io-extra","graphql-tools-v2","next-auth-v5","prisma-client-v2","tailwind-utils","react-query-v4","jest-extended-v2"]),this.phantomAPIs=[{pattern:/fs\.readFileAsync\s*\(/,suggestion:"Use fs.promises.readFile() or fs.readFileSync()",name:"fs.readFileAsync",id:"HALL_API001"},{pattern:/fs\.writeFileAsync\s*\(/,suggestion:"Use fs.promises.writeFile() or fs.writeFileSync()",name:"fs.writeFileAsync",id:"HALL_API002"},{pattern:/fs\.existsAsync\s*\(/,suggestion:"Use fs.promises.access() or fs.existsSync()",name:"fs.existsAsync",id:"HALL_API003"},{pattern:/path\.exists\s*\(/,suggestion:"Use fs.existsSync() — path module has no exists()",name:"path.exists",id:"HALL_API004"},{pattern:/path\.isFile\s*\(/,suggestion:"Use fs.statSync().isFile() — path module has no isFile()",name:"path.isFile",id:"HALL_API005"},{pattern:/path\.isDirectory\s*\(/,suggestion:"Use fs.statSync().isDirectory()",name:"path.isDirectory",id:"HALL_API006"},{pattern:/console\.success\s*\(/,suggestion:"Use console.log() — console.success() does not exist",name:"console.success",id:"HALL_API007"},{pattern:/console\.verbose\s*\(/,suggestion:"Use console.log() or console.debug()",name:"console.verbose",id:"HALL_API008"},{pattern:/JSON\.tryParse\s*\(/,suggestion:"Use try/catch with JSON.parse()",name:"JSON.tryParse",id:"HALL_API009"},{pattern:/JSON\.safeStringify\s*\(/,suggestion:"Use JSON.stringify() with a replacer",name:"JSON.safeStringify",id:"HALL_API010"},{pattern:/Array\.flatten\s*\(/,suggestion:"Use Array.prototype.flat()",name:"Array.flatten",id:"HALL_API011"},{pattern:/Object\.deepClone\s*\(/,suggestion:"Use structuredClone() or JSON.parse(JSON.stringify())",name:"Object.deepClone",id:"HALL_API012"},{pattern:/Object\.deepMerge\s*\(/,suggestion:"Use a library like lodash.merge or write a custom function",name:"Object.deepMerge",id:"HALL_API013"},{pattern:/String\.prototype\.replaceAll\s*&&.*polyfill/i,suggestion:"replaceAll() is native since ES2021 — no polyfill needed",name:"replaceAll polyfill",id:"HALL_API014"},{pattern:/require\s*\(\s*['"]fs\/sync['"]\s*\)/,suggestion:"fs/sync is not a real module — use fs directly",name:"fs/sync",id:"HALL_API015"},{pattern:/require\s*\(\s*['"]http\/server['"]\s*\)/,suggestion:"http/server is not a real module — use http.createServer()",name:"http/server",id:"HALL_API016"},{pattern:/process\.env\.get\s*\(/,suggestion:"Use process.env.VAR_NAME — process.env is a plain object, not a Map",name:"process.env.get()",id:"HALL_API017"},{pattern:/process\.exit\s*\(\s*['"]/,suggestion:"process.exit() takes a number, not a string",name:"process.exit(string)",id:"HALL_API018"}],this.pythonPhantomAPIs=[{pattern:/os\.path\.exists_sync\s*\(/,suggestion:"Use os.path.exists() — exists_sync does not exist in Python",name:"os.path.exists_sync",id:"PY_HALL001"},{pattern:/json\.tryParse\s*\(/,suggestion:"Use json.loads() with try/except",name:"json.tryParse",id:"PY_HALL002"},{pattern:/\w+\.flatMap\s*\(/,suggestion:"Python lists have no flatMap — use list comprehension or itertools.chain.from_iterable()",name:"list.flatMap",id:"PY_HALL003"},{pattern:/\w+\.merge\s*\((?!.*\bself\b)/,suggestion:"Use {**dict1, **dict2} or dict1 | dict2 (3.9+)",name:"dict.merge",id:"PY_HALL004"},{pattern:/\w+\.format_map\s*\(/,suggestion:"str.format_map exists but is rarely correct — did you mean .format()?",name:"string.format_map misuse",id:"PY_HALL005"},{pattern:/from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i,suggestion:"Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary",name:"Unnecessary OrderedDict",id:"PY_HALL006"},{pattern:/async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i,suggestion:"asyncio.sleep(0) to yield is a code smell — review async design",name:"asyncio.sleep(0) hack",id:"PY_HALL007"},{pattern:/import\s+tensorflow\.v2/,suggestion:"tensorflow.v2 is not a real module — use import tensorflow",name:"tensorflow.v2",id:"PY_HALL008"},{pattern:/from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/,suggestion:"train_test_split_v2 does not exist — use train_test_split",name:"sklearn v2 hallucination",id:"PY_HALL009"},{pattern:/requests\.async_get\s*\(/,suggestion:"requests has no async_get — use aiohttp or httpx",name:"requests.async_get",id:"PY_HALL010"}],this.pythonDeprecated=[{pattern:/from\s+distutils/,suggestion:"distutils removed in Python 3.12 — use setuptools",since:"Python 3.12",name:"distutils",id:"PY_DEPR001"},{pattern:/from\s+imp\s+import/,suggestion:"imp removed in Python 3.12 — use importlib",since:"Python 3.12",name:"imp module",id:"PY_DEPR002"},{pattern:/asyncio\.get_event_loop\(\)/,suggestion:"Deprecated in 3.10+ — use asyncio.run() or get_running_loop()",since:"Python 3.10",name:"asyncio.get_event_loop()",id:"PY_DEPR003"},{pattern:/collections\.MutableMapping/,suggestion:"Moved to collections.abc.MutableMapping in Python 3.3+",since:"Python 3.9",name:"collections.MutableMapping",id:"PY_DEPR004"},{pattern:/optparse\.OptionParser/,suggestion:"optparse deprecated since Python 3.2 — use argparse",since:"Python 3.2",name:"optparse",id:"PY_DEPR005"},{pattern:/cgi\.parse_header\s*\(/,suggestion:"cgi module deprecated in Python 3.11, removed in 3.13",since:"Python 3.11",name:"cgi module",id:"PY_DEPR006"},{pattern:/from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/,suggestion:"Use built-in list, dict, tuple, set for type hints (Python 3.9+)",since:"Python 3.9",name:"typing.List/Dict/Tuple",id:"PY_DEPR007"},{pattern:/unittest\.makeSuite\s*\(/,suggestion:"makeSuite deprecated — use TestLoader.loadTestsFromTestCase",since:"Python 3.11",name:"unittest.makeSuite",id:"PY_DEPR008"}],this.pythonSmells=[{pattern:/print\s*\(\s*f?['"]\s*debug/i,id:"PY_SMELL001",name:"Debug Print",message:"Debug print statement left in code"},{pattern:/except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m,id:"PY_SMELL002",name:"Bare Except",message:"Bare except or swallowed exception — hides real errors"},{pattern:/#\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PY_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError\s*\(?\s*\)?/,id:"PY_SMELL004",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i,id:"PY_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PY_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/import\s+\*/,id:"PY_SMELL007",name:"Wildcard Import",message:"Wildcard import — pollutes namespace, hides dependencies"}],this.goDeprecated=[{pattern:/ioutil\.ReadFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.ReadFile()",since:"Go 1.16",name:"ioutil.ReadFile",id:"GO_DEPR001"},{pattern:/ioutil\.WriteFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.WriteFile()",since:"Go 1.16",name:"ioutil.WriteFile",id:"GO_DEPR002"},{pattern:/ioutil\.TempDir\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.MkdirTemp()",since:"Go 1.16",name:"ioutil.TempDir",id:"GO_DEPR003"},{pattern:/ioutil\.ReadAll\s*\(/,suggestion:"ioutil removed in Go 1.16 — use io.ReadAll()",since:"Go 1.16",name:"ioutil.ReadAll",id:"GO_DEPR004"},{pattern:/strings\.Title\s*\(/,suggestion:"strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text",since:"Go 1.18",name:"strings.Title",id:"GO_DEPR005"}],this.goSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"GO_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/panic\s*\(\s*["']not implemented["']\s*\)/,id:"GO_SMELL002",name:"Panic Not Implemented",message:"Stub implementation — will panic at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"GO_SMELL003",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.rustPhantomAPIs=[{pattern:/std::fs::File::open_async\s*\(/,suggestion:"Use tokio::fs::File::open() for async file I/O",name:"File::open_async",id:"RS_HALL001"},{pattern:/Vec::flatten\s*\(/,suggestion:"Use .into_iter().flatten() — Vec has no flatten() method",name:"Vec::flatten",id:"RS_HALL002"},{pattern:/HashMap::get_or_default\s*\(/,suggestion:"Use .get().unwrap_or(&default) or entry().or_insert()",name:"HashMap::get_or_default",id:"RS_HALL003"},{pattern:/String::from_utf8_lossy_owned\s*\(/,suggestion:"Use String::from_utf8_lossy().into_owned()",name:"String::from_utf8_lossy_owned",id:"RS_HALL004"},{pattern:/\.async_iter\s*\(/,suggestion:"Use futures::stream::iter() from the futures crate",name:".async_iter()",id:"RS_HALL005"},{pattern:/std::net::TcpStream::connect_async\s*\(/,suggestion:"Use tokio::net::TcpStream::connect()",name:"TcpStream::connect_async",id:"RS_HALL006"},{pattern:/std::thread::sleep_async\s*\(/,suggestion:"Use tokio::time::sleep() for async sleep",name:"thread::sleep_async",id:"RS_HALL007"},{pattern:/Vec::remove_all\s*\(/,suggestion:"Use .retain(|x| condition) or .clear()",name:"Vec::remove_all",id:"RS_HALL008"},{pattern:/str::split_whitespace_n\s*\(/,suggestion:"Use .splitn(n, char::is_whitespace) instead",name:"str::split_whitespace_n",id:"RS_HALL009"},{pattern:/\.map_async\s*\(/,suggestion:"Use futures::future::join_all() or tokio::task::spawn",name:".map_async()",id:"RS_HALL010"}],this.rustDeprecated=[{pattern:/std::sync::ONCE_INIT/,suggestion:"std::sync::ONCE_INIT removed — use Once::new()",since:"Rust 1.38",name:"ONCE_INIT",id:"RS_DEPR001"},{pattern:/\bstd::mem::uninitialized\s*\(/,suggestion:"std::mem::uninitialized() removed — use MaybeUninit::uninit()",since:"Rust 1.39",name:"mem::uninitialized",id:"RS_DEPR002"},{pattern:/\btry!\s*\(/,suggestion:"try!() macro removed — use the ? operator",since:"Rust 2018",name:"try!() macro",id:"RS_DEPR003"},{pattern:/extern\s+crate\s+std\s*;/,suggestion:"extern crate std is implicit since Rust 2018 edition",since:"Rust 2018",name:"extern crate std",id:"RS_DEPR004"},{pattern:/extern\s+crate\s+alloc\s*;/,suggestion:"extern crate alloc is implicit in 2018+ edition",since:"Rust 2018",name:"extern crate alloc",id:"RS_DEPR005"},{pattern:/std::error::Error::cause\s*\(/,suggestion:".cause() deprecated — use .source() instead",since:"Rust 1.33",name:"Error::cause()",id:"RS_DEPR006"}],this.rustSmells=[{pattern:/todo!\s*\(\s*\)/,id:"RS_SMELL001",name:"todo! macro",message:"todo!() macro — will panic at runtime"},{pattern:/unimplemented!\s*\(\s*\)/,id:"RS_SMELL002",name:"unimplemented! macro",message:"unimplemented!() macro — will panic at runtime"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RS_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/unwrap\(\).*unwrap\(\)/,id:"RS_SMELL004",name:"Double Unwrap",message:"Chained unwrap() — will panic on None/Err"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"RS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/unsafe\s*\{/,id:"RS_SMELL006",name:"Unsafe Block",message:"unsafe block — verify memory safety guarantees"},{pattern:/\.unwrap\(\)\s*;/,id:"RS_SMELL007",name:"Bare unwrap()",message:".unwrap() will panic on None/Err — use ? or match"},{pattern:/println!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL008",name:"Debug println!",message:"Debug println! left in code — use the log crate"},{pattern:/eprintln!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL009",name:"Debug eprintln!",message:"Debug eprintln! left in code — use the log crate"},{pattern:/#\[allow\(dead_code\)\]/,id:"RS_SMELL010",name:"Suppressed Dead Code",message:"#[allow(dead_code)] suppresses warnings — remove unused code"}],this.javaSmells=[{pattern:/System\.out\.println\s*\(.*debug/i,id:"JAVA_SMELL001",name:"Debug Println",message:"Debug System.out.println — use a logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"JAVA_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i,id:"JAVA_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"JAVA_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"JAVA_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.csharpSmells=[{pattern:/Console\.WriteLine\s*\(.*[Dd]ebug/i,id:"CS_SMELL001",name:"Debug WriteLine",message:"Debug Console.WriteLine — use ILogger or Debug.Write"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"CS_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:NotImplementedException)\s*\(/,id:"CS_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"CS_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"CS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.kotlinPhantomAPIs=[{pattern:/\.flatMap\s*\{[^}]*\}(?!.*asSequence)/,suggestion:"Use .flatMap { } — verify this is not confusing sequence vs list behavior",name:"flatMap confusion",id:"KT_HALL001"},{pattern:/listOf\(\)\.stream\s*\(/,suggestion:"Kotlin lists have no .stream() — use .asSequence() or direct collection ops",name:"listOf().stream()",id:"KT_HALL002"},{pattern:/String\.format\s*\(/,suggestion:'Prefer Kotlin string templates "$variable" over String.format()',name:"String.format Java-style",id:"KT_HALL003"},{pattern:/\bObject\s*\(\s*\)/,suggestion:"Kotlin uses Any() not Object() — or use object keyword for singletons",name:"Object() Java-style",id:"KT_HALL004"},{pattern:/\.forEach\s*\(\s*::println\s*\)(?!.*debug)/i,suggestion:"println is for debug output — use a logger",name:"println via forEach",id:"KT_HALL005"},{pattern:/coroutineScope\s*\{\s*launch\s*\{[^}]*\}\s*\}(?!\s*\.join)/,suggestion:"Unjoined coroutine in coroutineScope may cause issues — ensure structured concurrency",name:"Unawaited launch",id:"KT_HALL006"},{pattern:/Dispatchers\.IO\s*\+\s*Dispatchers/,suggestion:"Combining Dispatchers is not valid — use one dispatcher at a time",name:"Combined Dispatchers",id:"KT_HALL007"},{pattern:/\.toList\(\)\.stream\(\)/,suggestion:"Use Kotlin sequences (.asSequence()) instead of .toList().stream()",name:"toList().stream()",id:"KT_HALL008"},{pattern:/companion object.*getInstance/i,suggestion:"Kotlin idiom for singletons is object keyword, not companion object + getInstance()",name:"Java-style singleton",id:"KT_HALL009"},{pattern:/lateinit var.*\?/,suggestion:"lateinit properties cannot be nullable — remove ? or use by lazy {}",name:"lateinit nullable",id:"KT_HALL010"}],this.kotlinDeprecated=[{pattern:/\bapply\s+plugin:\s*['"]kotlin-android-extensions['"]/,suggestion:"kotlin-android-extensions deprecated — use view binding or Jetpack ViewBinding",since:"Kotlin 1.8",name:"kotlin-android-extensions",id:"KT_DEPR001"},{pattern:/\bkotlinx\.android\.synthetic/,suggestion:"Synthetic imports deprecated — use view binding or findViewById",since:"Kotlin 1.8",name:"Synthetic imports",id:"KT_DEPR002"},{pattern:/\bCoroutineScope\(EmptyCoroutineContext\)/,suggestion:"EmptyCoroutineContext coroutine scope is error-prone — use viewModelScope or lifecycleScope",since:"Kotlin Coroutines 1.6",name:"EmptyCoroutineContext scope",id:"KT_DEPR003"},{pattern:/\bBuildersKt\.launch\b/,suggestion:"Internal BuildersKt APIs are deprecated — use coroutineScope { launch {} }",since:"Kotlin Coroutines 1.5",name:"BuildersKt.launch",id:"KT_DEPR004"},{pattern:/\basyncLazy\s*\{/,suggestion:"asyncLazy {} is not a standard Kotlin API — use lazy {} or async {}",since:"Kotlin 1.6",name:"asyncLazy",id:"KT_DEPR005"}],this.kotlinSmells=[{pattern:/\bprintln\s*\(/,id:"KT_SMELL001",name:"Debug println",message:"println() is for debug output — use a proper logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"KT_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/TODO\s*\(\s*\)/,id:"KT_SMELL003",name:"TODO() stub",message:"TODO() will throw NotImplementedError at runtime"},{pattern:/!!\s*\./,id:"KT_SMELL004",name:"Non-null assertion chain",message:"!! non-null assertion — will throw NullPointerException if null"},{pattern:/\bas\s+\w+\b(?!.*\?)/,suggestion:"Unsafe cast — prefer safe cast (as? Type) with null check",id:"KT_SMELL005",name:"Unsafe cast",message:"Unsafe cast with as — will throw ClassCastException if wrong type"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"KT_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/catch\s*\(\s*e:\s*Exception\s*\)\s*\{\s*\}/,id:"KT_SMELL007",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/throw\s+NotImplementedError\s*\(/,id:"KT_SMELL008",name:"Not Implemented",message:"Stub implementation — will throw at runtime"}],this.phpPhantomAPIs=[{pattern:/array_flatten\s*\(/,suggestion:"PHP has no array_flatten() — use array_merge(...$array) or a recursive function",name:"array_flatten",id:"PHP_HALL001"},{pattern:/str_contains_all\s*\(/,suggestion:"PHP has no str_contains_all() — use multiple str_contains() calls",name:"str_contains_all",id:"PHP_HALL002"},{pattern:/array_unique_values\s*\(/,suggestion:"PHP has no array_unique_values() — use array_values(array_unique())",name:"array_unique_values",id:"PHP_HALL003"},{pattern:/\$pdo->fetchAll\s*\(/,suggestion:"fetchAll() is on PDOStatement not PDO — call $stmt->fetchAll()",name:"PDO::fetchAll",id:"PHP_HALL004"},{pattern:/json_decode_safe\s*\(/,suggestion:"PHP has no json_decode_safe() — wrap json_decode() with json_last_error() check",name:"json_decode_safe",id:"PHP_HALL005"},{pattern:/str_replace_all\s*\(/,suggestion:"PHP has no str_replace_all() — use str_replace() which already replaces all occurrences",name:"str_replace_all",id:"PHP_HALL006"},{pattern:/array_map_keys\s*\(/,suggestion:"PHP has no array_map_keys() — use array_combine(array_map(...), array_keys())",name:"array_map_keys",id:"PHP_HALL007"},{pattern:/\$request->getJson\s*\(/,suggestion:'Not a standard PHP function — use json_decode(file_get_contents("php://input"))',name:"$request->getJson",id:"PHP_HALL008"},{pattern:/Date::now\s*\(/,suggestion:"PHP has no Date::now() — use new DateTime() or time()",name:"Date::now",id:"PHP_HALL009"},{pattern:/\bawait\s+/,suggestion:"PHP has no await keyword — use synchronous code or ReactPHP for async",name:"await keyword",id:"PHP_HALL010"}],this.phpDeprecated=[{pattern:/\bmysql_connect\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_connect",id:"PHP_DEPR001"},{pattern:/\bmysql_query\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_query",id:"PHP_DEPR002"},{pattern:/\bmysql_fetch_array\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_fetch_array",id:"PHP_DEPR003"},{pattern:/\bereg\s*\(/,suggestion:"ereg() removed in PHP 7 — use preg_match()",since:"PHP 7.0",name:"ereg()",id:"PHP_DEPR004"},{pattern:/\bsplit\s*\(/,suggestion:"split() removed in PHP 7 — use preg_split() or explode()",since:"PHP 7.0",name:"split()",id:"PHP_DEPR005"},{pattern:/\bcreate_function\s*\(/,suggestion:"create_function() removed in PHP 8 — use anonymous functions (closures)",since:"PHP 8.0",name:"create_function",id:"PHP_DEPR006"},{pattern:/\bmagic_quotes_gpc\b/,suggestion:"magic_quotes_gpc removed in PHP 7 — sanitize inputs manually",since:"PHP 7.0",name:"magic_quotes_gpc",id:"PHP_DEPR007"},{pattern:/\beach\s*\(/,suggestion:"each() deprecated in PHP 7.2, removed in PHP 8 — use foreach",since:"PHP 8.0",name:"each()",id:"PHP_DEPR008"},{pattern:/\bmcrypt_/,suggestion:"mcrypt removed in PHP 7.2 — use OpenSSL functions",since:"PHP 7.2",name:"mcrypt functions",id:"PHP_DEPR009"}],this.phpSmells=[{pattern:/\beval\s*\(/,id:"PHP_SMELL001",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bextract\s*\(\s*\$_(?:GET|POST|REQUEST)/,id:"PHP_SMELL002",name:"extract() from superglobal",message:"extract($_GET/_POST) — variable injection vulnerability"},{pattern:/\$\$\w+/,id:"PHP_SMELL003",name:"Variable variable",message:"Variable variables ($$var) — hard to trace and injection risk"},{pattern:/\bshell_exec\s*\(/,id:"PHP_SMELL004",name:"shell_exec()",message:"shell_exec() — remote code execution risk if input unsanitized"},{pattern:/\bsystem\s*\(/,id:"PHP_SMELL005",name:"system()",message:"system() — remote code execution risk if input unsanitized"},{pattern:/\bpassthru\s*\(/,id:"PHP_SMELL006",name:"passthru()",message:"passthru() — remote code execution risk if input unsanitized"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PHP_SMELL007",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PHP_SMELL008",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/\$_(?:GET|POST|REQUEST)\[.*\].*SELECT.*\./i,id:"PHP_SMELL009",name:"SQL Injection Risk",message:"User input directly in SQL string — use prepared statements"},{pattern:/\bvar_dump\s*\(/,id:"PHP_SMELL010",name:"Debug var_dump",message:"var_dump() is for debug only — remove before production"}],this.rubyPhantomAPIs=[{pattern:/\.flatten_map\s*\{/,suggestion:"Ruby has no .flatten_map — use .flat_map { }",name:".flatten_map",id:"RB_HALL001"},{pattern:/Array\.of\s*\(/,suggestion:"Ruby has no Array.of() — use Array() or []",name:"Array.of()",id:"RB_HALL002"},{pattern:/Hash\.from_array\s*\(/,suggestion:"Ruby has no Hash.from_array() — use array.to_h or Hash[array]",name:"Hash.from_array",id:"RB_HALL003"},{pattern:/\.async\s*\{/,suggestion:"Ruby has no .async {} — use Thread.new or concurrent-ruby gem",name:".async block",id:"RB_HALL004"},{pattern:/String\.format\s*\(/,suggestion:"Ruby has no String.format() — use string interpolation or % operator",name:"String.format",id:"RB_HALL005"},{pattern:/\bpromise\s*\{/,suggestion:"Ruby has no built-in promise {} — use concurrent-ruby gem or Thread",name:"promise block",id:"RB_HALL006"},{pattern:/\.try!\s*\(/,suggestion:"Ruby has no .try!() — use &. (safe navigation) or Rails .try()",name:".try!()",id:"RB_HALL007"},{pattern:/JSON\.safe_parse\s*\(/,suggestion:"Ruby has no JSON.safe_parse — use JSON.parse with rescue",name:"JSON.safe_parse",id:"RB_HALL008"},{pattern:/ActiveRecord::Base\.execute\s*\(/,suggestion:"ActiveRecord::Base has no .execute() — use connection.execute() or where()",name:"Base.execute",id:"RB_HALL009"},{pattern:/\.collect_map\s*\{/,suggestion:"Ruby has no .collect_map — use .map { }.flatten(1) or .flat_map",name:".collect_map",id:"RB_HALL010"}],this.rubyDeprecated=[{pattern:/\brequire\s+['"]thread['"]/,suggestion:'require "thread" is deprecated — Thread is now built-in without require',since:"Ruby 2.0",name:'require "thread"',id:"RB_DEPR001"},{pattern:/\bObject#type\b|\.type\s*==/,suggestion:".type is deprecated — use .class or .is_a?",since:"Ruby 1.8",name:"Object#type",id:"RB_DEPR002"},{pattern:/\bERB::Util\.html_escape\b/,suggestion:"ERB::Util.html_escape deprecated — use CGI.escapeHTML or h() in views",since:"Ruby 2.6",name:"ERB::Util.html_escape",id:"RB_DEPR003"},{pattern:/\bFile\.exists\?\s*\(/,suggestion:"File.exists? deprecated — use File.exist? (no s)",since:"Ruby 2.2",name:"File.exists?",id:"RB_DEPR004"},{pattern:/\bDir\.exists\?\s*\(/,suggestion:"Dir.exists? deprecated — use Dir.exist? (no s)",since:"Ruby 2.2",name:"Dir.exists?",id:"RB_DEPR005"},{pattern:/\bObject#returning\b|\s+returning\s+/,suggestion:"Object#returning removed from Rails core — use tap { |obj| }",since:"Rails 3.0",name:"Object#returning",id:"RB_DEPR006"}],this.rubySmells=[{pattern:/\bputs\s+(?!STDOUT)/,id:"RB_SMELL001",name:"Debug puts",message:"puts is for debug output — use Rails logger or a logging library"},{pattern:/\bp\s+\w/,id:"RB_SMELL002",name:"Debug p()",message:"p() is for debug output — remove before production"},{pattern:/\beval\s*\(/,id:"RB_SMELL003",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bsystem\s*\(/,id:"RB_SMELL004",name:"system() Shell Call",message:"system() call — command injection risk if input unsanitized"},{pattern:/`[^`]+`/,id:"RB_SMELL005",name:"Backtick Shell Exec",message:"Backtick shell execution — command injection risk"},{pattern:/\bexec\s*\(/,id:"RB_SMELL006",name:"exec() call",message:"exec() replaces the process — ensure this is intentional"},{pattern:/\brescue\s*$|\brescue\s+Exception\b/,id:"RB_SMELL007",name:"Bare rescue",message:"Bare rescue catches all exceptions including fatal ones — be specific"},{pattern:/["'].*#\{.*sql.*\}.*["']/i,id:"RB_SMELL008",name:"SQL String Interpolation",message:"SQL built with string interpolation — use parameterized queries"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RB_SMELL009",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError/,id:"RB_SMELL010",name:"Not Implemented",message:"Stub implementation — will raise at runtime"}],this.jsExtensions=new Set([".js",".ts",".jsx",".tsx",".mjs",".cjs"]),this.pyExtensions=new Set([".py",".pyw"]),this.goExtensions=new Set([".go"]),this.rustExtensions=new Set([".rs"]),this.javaExtensions=new Set([".java",".kt"]),this.csharpExtensions=new Set([".cs"]),this.phpExtensions=new Set([".php"]),this.rubyExtensions=new Set([".rb"]),this.aiCodeSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i,id:"AI_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished implementation"},{pattern:/throw new Error\(['"]Not implemented['"]\)/,id:"AI_SMELL002",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/\/\/\s*\.\.\.\s*(rest|remaining|more|other|additional)/i,id:"AI_SMELL003",name:"Truncated Code",message:"AI output was truncated — code is incomplete"},{pattern:/\/\/\s*(your|replace|insert|put)\s+(code|logic|implementation|api[_\s]?key)/i,id:"AI_SMELL004",name:"Template Placeholder",message:"Template placeholder left in code — needs real implementation"},{pattern:/['"]your-api-key-here['"]|['"]sk-[.]{3,}['"]|['"]xxx+['"]/i,id:"AI_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/example\.com|test@test\.com|john@doe\.com|foo@bar\.com/i,id:"AI_SMELL006",name:"Example Domain",message:"Example domain/email in production code"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"AI_SMELL007",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.deprecatedAPIs=[{pattern:/new Buffer\s*\(/,suggestion:"Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()",since:"Node 6",name:"new Buffer()",id:"DEPR_API001"},{pattern:/require\s*\(\s*['"]sys['"]\s*\)/,suggestion:'Use require("util") — sys was removed in Node 1.0',since:"Node 1.0",name:'require("sys")',id:"DEPR_API002"},{pattern:/fs\.exists\s*\((?!Sync)/,suggestion:"Use fs.access() or fs.stat() — fs.exists() is deprecated",since:"Node 1.0",name:"fs.exists()",id:"DEPR_API003"},{pattern:/domain\.create\s*\(/,suggestion:"Use async_hooks or try/catch — domain module is deprecated",since:"Node 4",name:"domain.create()",id:"DEPR_API004",fixable:!1},{pattern:/require\s*\(\s*['"]punycode['"]\s*\)/,suggestion:"Use userland punycode package — built-in is deprecated",since:"Node 7",name:'require("punycode")',id:"DEPR_API005",fixable:!1},{pattern:/url\.parse\s*\(/,suggestion:"Use new URL() — url.parse() is legacy",since:"Node 11",name:"url.parse()",id:"DEPR_API006"},{pattern:/querystring\.(parse|stringify)\s*\(/,suggestion:"Use URLSearchParams — querystring is legacy",since:"Node 14",name:"querystring",id:"DEPR_API007",fixable:!1},{pattern:/util\.isArray\s*\(/,suggestion:"Use Array.isArray() — util type checks are deprecated",since:"Node 4",name:"util.isArray()",id:"DEPR_API008"},{pattern:/util\.isFunction\s*\(/,suggestion:'Use typeof fn === "function"',since:"Node 4",name:"util.isFunction()",id:"DEPR_API009"},{pattern:/util\.pump\s*\(/,suggestion:"Use stream.pipeline() — util.pump() was removed",since:"Node 1.0",name:"util.pump()",id:"DEPR_API010",fixable:!1}]}async scan(e){const t=Date.now(),s={phantomImports:[],phantomAPIs:[],aiSmells:[],deprecatedAPIs:[],unresolvedDeps:[],stats:{filesScanned:0,totalIssues:0,scanTime:0}},n=this._loadDeclaredDeps(),i=this._loadIgnoreRules();for(const t of e)try{const e=path.extname(t).toLowerCase(),a=this.jsExtensions.has(e),o=this.pyExtensions.has(e),r=this.goExtensions.has(e),l=this.rustExtensions.has(e),c=this.javaExtensions.has(e),d=this.csharpExtensions.has(e),p=this.phpExtensions.has(e),m=this.rubyExtensions.has(e);if(!(a||o||r||l||c||d||p||m))continue;const u=path.relative(this.config.rootPath,t);if(i.some(e=>u.includes(e)||t.includes(e)))continue;const g=fs.readFileSync(t,"utf-8"),h=g.split("\n"),_=new Set;for(let e=0;e<h.length;e++)h[e].includes("thuban-ignore")&&(_.add(e+1),_.add(e+2));const f=this._isPatternDefinitionFile(g);s.stats.filesScanned++,a?(this._checkPhantomImports(u,g,h,n,s,_),f||this._checkPhantomAPIs(u,g,h,s,_),this._checkAISmells(u,g,h,s,_),f||this._checkDeprecatedAPIs(u,g,h,s,_),f||this._checkNamedExports(u,t,g,s,_)):o?(this._checkLanguagePatterns(u,g,h,s,_,this.pythonPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.pythonDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.pythonSmells)):p?(this._checkLanguagePatterns(u,g,h,s,_,this.phpPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.phpDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.phpSmells)):m?(this._checkLanguagePatterns(u,g,h,s,_,this.rubyPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rubyDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rubySmells)):r?(this._checkLanguagePatterns(u,g,h,s,_,this.goDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.goSmells)):l?(this._checkLanguagePatterns(u,g,h,s,_,this.rustPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rustDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rustSmells)):c?".kt"===e?(this._checkLanguagePatterns(u,g,h,s,_,this.kotlinPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.kotlinDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.kotlinSmells)):this._checkLanguageSmells(u,g,h,s,_,this.javaSmells):d&&this._checkLanguageSmells(u,g,h,s,_,this.csharpSmells)}catch(e){}return s.stats.totalIssues=s.phantomImports.length+s.phantomAPIs.length+s.aiSmells.length+s.deprecatedAPIs.length+s.unresolvedDeps.length,s.stats.scanTime=Date.now()-t,s}_safeRegexTest(e,t,s=1e3){const n=Date.now();try{const i=e.test(t),a=Date.now()-n;return a>s?(console.warn(`[HALLUCINATION-DETECTOR] WARNING: Regex ${e.source.substring(0,50)}... took ${a}ms on input (${t.length} chars) — skipping pattern`),null):i}catch(e){return console.warn("[HALLUCINATION-DETECTOR] Regex error during scan"),null}}_safeRegexExec(e,t,s=1e3){const n=Date.now();try{const i=e.exec(t),a=Date.now()-n;return a>s?(console.warn(`[HALLUCINATION-DETECTOR] WARNING: Regex exec ${e.source.substring(0,50)}... took ${a}ms — skipping pattern`),null):i}catch(e){return console.warn("[HALLUCINATION-DETECTOR] Regex exec error during scan"),null}}_checkLanguagePatterns(e,t,s,n,i,a,o){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const r=s[t];for(const s of a){const i=this._safeRegexTest(s.pattern,r);if(null!==i&&i){const i={file:e,line:t+1,name:s.name,id:s.id};s.suggestion&&(i.suggestion=s.suggestion),s.since&&(i.since=s.since,i.fix=s.suggestion),"deprecatedAPIs"===o?n.deprecatedAPIs.push(i):n.phantomAPIs.push(i)}}}}_checkLanguageSmells(e,t,s,n,i,a){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const o=s[t];for(const s of a){const i=this._safeRegexTest(s.pattern,o);null!==i&&i&&n.aiSmells.push({file:e,line:t+1,id:s.id,name:s.name,message:s.message,type:"ai_smell",severity:"medium"})}}}formatReport(e){const t="",s="",n="",i="",a="",o="",r="",l="";let c="";if(c+=`\n${l} ╔══════════════════════════════════════════════╗${t}\n`,c+=`${l} ║${s} THUBAN HALLUCINATION REPORT ${t}${l}║${t}\n`,c+=`${l} ╚══════════════════════════════════════════════╝${t}\n\n`,0===e.stats.totalIssues)return c+=` ${i}${s}No hallucinations detected.${t} ${r}Clean codebase.${t}\n\n`,c;if(e.phantomImports.length>0){c+=` ${n}${s}Phantom Imports (${e.phantomImports.length})${t}\n`,c+=` ${r}Packages that don't exist in package.json or node_modules${t}\n\n`;for(const s of e.phantomImports.slice(0,10))c+=` ${n}✗${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.module}${t} ${r}— ${s.reason}${t}\n`;e.phantomImports.length>10&&(c+=` ${r}... and ${e.phantomImports.length-10} more${t}\n`),c+="\n"}if(e.phantomAPIs.length>0){c+=` ${n}${s}Phantom APIs (${e.phantomAPIs.length})${t}\n`,c+=` ${r}Methods/properties that don't exist on their objects${t}\n\n`;for(const s of e.phantomAPIs.slice(0,10))c+=` ${n}✗${t} ${s.id?`${r}[${s.id}]${t} `:""}${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}— ${s.suggestion}${t}\n`,c+=` ${r}Suppress: // thuban-ignore ${s.id||"HALL_API"}${t}\n`;e.phantomAPIs.length>10&&(c+=` ${r}... and ${e.phantomAPIs.length-10} more${t}\n`),c+="\n"}if(e.aiSmells.length>0){c+=` ${a}${s}AI Code Smells (${e.aiSmells.length})${t}\n`,c+=` ${r}Patterns typical of AI-generated code that needs review${t}\n\n`;for(const s of e.aiSmells.slice(0,10))c+=` ${a}!${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${r}${s.message}${t}\n`;e.aiSmells.length>10&&(c+=` ${r}... and ${e.aiSmells.length-10} more${t}\n`),c+="\n"}if(e.deprecatedAPIs.length>0){c+=` ${a}${s}Deprecated APIs (${e.deprecatedAPIs.length})${t}\n`,c+=` ${r}APIs that LLMs still suggest but are deprecated or removed${t}\n\n`;for(const s of e.deprecatedAPIs.slice(0,10))c+=` ${a}⚠${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}(deprecated since ${s.since})${t}\n`,c+=` ${i}→${t} ${r}${s.suggestion}${t}\n`;e.deprecatedAPIs.length>10&&(c+=` ${r}... and ${e.deprecatedAPIs.length-10} more${t}\n`),c+="\n"}return c+=` ${s}Summary:${t} ${o}${e.stats.filesScanned}${t} files scanned, `,c+=`${e.stats.totalIssues>0?n:i}${e.stats.totalIssues}${t} hallucination issues found `,c+=`${r}(${e.stats.scanTime}ms)${t}\n\n`,c}_loadDeclaredDeps(){const e=new Set;return this._packageJsonPaths=new Map,this._collectPackageJsonDeps(this.config.rootPath,e,0),e}_collectPackageJsonDeps(e,t,s){if(!(s>5))try{const n=path.join(e,"package.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf-8")),i=new Set;for(const e of Object.keys(s.dependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.devDependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.peerDependencies||{}))t.add(e),i.add(e);this._packageJsonPaths.set(e,i)}const i=new Set(["node_modules",".git","dist","build","coverage",".next",".cache","vendor"]),a=fs.readdirSync(e,{withFileTypes:!0});for(const n of a)!n.isDirectory()||i.has(n.name)||n.name.startsWith(".")||this._collectPackageJsonDeps(path.join(e,n.name),t,s+1)}catch(e){}}_checkPhantomImports(e,t,s,n,i,a=new Set){const o=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const s of o){let o;for(;null!==(o=s.exec(t));){const s=o[1];if(s.startsWith(".")||s.startsWith("/"))continue;const r=s.split("/")[0];if(this.builtins.has(s)||this.builtins.has(r))continue;const l=s.startsWith("@")?s.split("/").slice(0,2).join("/"):r;if(!n.has(l)&&!this._findInNodeModules(file,l)){const n=this._findLineNumber(t,o.index);if(a.has(n))continue;const r=this.phantomPackages.has(s);i.phantomImports.push({file:e,line:n,module:s,reason:r?"Known AI-hallucinated package — does not exist on npm":"Not in package.json and not in node_modules",severity:r?"critical":"high",fixable:!1})}}}}_checkNamedExports(e,t,s,n,i=new Set){let a;try{a=exportVerifier.verifyFile(t,s)}catch(e){return}for(const t of a)i.has(t.line)||n.phantomImports.push({file:e,line:t.line,module:t.source,reason:t.message,severity:"critical",fixable:!1,source:"export-verifier"})}_checkPhantomAPIs(e,t,s,n,i=new Set){for(const a of this.phantomAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.phantomAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,severity:"high",fixable:!0,fixAction:"replace_phantom_api"})}}}_checkAISmells(e,t,s,n,i=new Set){for(const a of this.aiCodeSmells){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);i.has(r)||n.aiSmells.push({file:e,line:r,id:a.id,name:a.name,message:a.message,severity:"warning",code:s[r-1]?.trim().substring(0,100)})}}}_checkDeprecatedAPIs(e,t,s,n,i=new Set){for(const a of this.deprecatedAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=this._safeRegexExec(r,t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.deprecatedAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,since:a.since,severity:"warning",fixable:!0,fixAction:"update_deprecated_api"})}}}_findLineNumber(e,t){let s=1;for(let n=0;n<t&&n<e.length;n++)"\n"===e[n]&&s++;return s}_isPatternDefinitionFile(e){const t=(e.match(/pattern:\s*\//g)||[]).length,s=(e.match(/phantom|hallucin/gi)||[]).length;return t>5&&s>2}_isInsidePattern(e,t){const s=e.trim();return!!(/pattern:\s*\//.test(s)||s.startsWith("//")||s.startsWith("*")||/(?:name|suggestion|message|description):\s*['"]/.test(s))}_loadIgnoreRules(){const e=[];try{const t=path.join(this.config.rootPath,".thubanrc.json"),s=JSON.parse(fs.readFileSync(t,"utf-8"));s.hallucination?.ignore&&e.push(...s.hallucination.ignore),s.ignore&&e.push(...s.ignore)}catch(e){}return e}_findInNodeModules(e,t){let s=path.dirname(e);const n=this.config.rootPath;for(;s.length>=n.length;){const e=path.join(s,"node_modules",t);try{if(fs.existsSync(e))return!0}catch(e){}const n=path.dirname(s);if(n===s)break;s=n}return!1}}module.exports=HallucinationDetector;
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),crypto=require("crypto"),EventEmitter=require("events"),{execFileSync:execFileSync}=require("child_process");class SecretScanner extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".next/**","coverage/**"],maxFileSize:e.maxFileSize||1048576,entropyThreshold:e.entropyThreshold||4.2,minEntropyLength:e.minEntropyLength||20,includeGitHistory:!1!==e.includeGitHistory,...e},this.scanExtensions=new Set([".env",".js",".jsx",".ts",".tsx",".py",".json",".yaml",".yml",".toml",".ini",".conf",".config",".properties",".sh",".bash",".zsh"]),this.ignoredDirectories=["node_modules",".git","dist",".next","coverage",".venv","venv","__pycache__"],this.safeLinePatterns=[/process\.env\./i,/process\.env\[/i,/os\.environ/i,/sample/i,/placeholder/i,/dummy/i,/mock/i,/example/i,/replace[_-\s]?me/i,/replace[_-\s]?with/i,/local-dev-token/i,/changeme/i,/your[_-\s]?key/i,/your[_-\s]?(secret|token|password)/i,/<secret>/i,/<token>/i,/x{8,}/i],this.secretPatterns=[{id:"SECRET001",type:"Stripe Secret Key",severity:"critical",recommendation:"Move this key to a cloud secret manager and load it via environment variables.",pattern:/\bsk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET002",type:"Stripe Publishable Key",severity:"high",recommendation:"Verify this publishable key is intended to be public; if not, move it to managed secrets.",pattern:/\bpk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET003",type:"AWS Access Key",severity:"critical",recommendation:"Rotate the AWS credential immediately and store replacements in a cloud secret manager.",pattern:/\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g},{id:"SECRET004",type:"GitHub Token",severity:"critical",recommendation:"Revoke the GitHub token and replace it with a managed secret or GitHub App credential.",pattern:/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g},{id:"SECRET005",type:"Slack Token",severity:"critical",recommendation:"Rotate the Slack token and move it into a cloud secret manager.",pattern:/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g},{id:"SECRET006",type:"Twilio Credential",severity:"critical",recommendation:"Rotate the Twilio credential and store it in managed secrets.",pattern:/\b(SK|AC)[0-9a-fA-F]{32}\b/g},{id:"SECRET007",type:"Google API Key",severity:"critical",recommendation:"Restrict and rotate the Google API key, then move it to a cloud secret manager.",pattern:/\bAIza[0-9A-Za-z\-_]{35}\b/g},{id:"SECRET007B",type:"OpenAI Project Key",severity:"critical",recommendation:"Rotate the OpenAI project key and move it to a cloud secret manager.",pattern:/\bsk-proj-[A-Za-z0-9\-_]{20,}\b/g},{id:"SECRET007C",type:"Cartesia API Key",severity:"critical",recommendation:"Rotate the Cartesia API key and move it to a cloud secret manager.",pattern:/\bsk_car_[A-Za-z0-9]{16,}\b/g},{id:"SECRET008",type:"JWT Token",severity:"high",recommendation:"Do not hardcode JWTs; issue them dynamically and store signing secrets in managed secrets.",pattern:/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9._-]{8,}\.[A-Za-z0-9._-]{8,}\b/g},{id:"SECRET009",type:"Bearer Token",severity:"high",recommendation:"Move bearer tokens to a cloud secret manager and inject them at runtime.",pattern:/\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/g},{id:"SECRET010",type:"Private Key",severity:"critical",recommendation:"Remove the private key from source control, rotate it, and store it in a secret manager.",pattern:/-----BEGIN (RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g},{id:"SECRET011",type:"Database Connection String with Password",severity:"critical",recommendation:"Move the connection string to managed secrets and avoid embedding passwords in code or config.",pattern:/\b(?:postgres(?:ql)?|mysql|mssql|mongodb(?:\+srv)?|redis):\/\/[^:\s]+:[^@\s]+@[^'"`\s]+\b/gi},{id:"SECRET012",type:"Webhook Secret",severity:"high",recommendation:"Store webhook secrets in a cloud secret manager and inject them via environment variables.",pattern:/\b(?:whsec_[A-Za-z0-9]+|webhook[_-]?secret[_:=\s'"]+[A-Za-z0-9_\-]{12,})\b/gi},{id:"SECRET013",type:"Hardcoded Password or Secret Assignment",severity:"high",recommendation:"Replace hardcoded credentials with environment variables backed by a cloud secret manager.",pattern:/\b(?:password|passwd|pwd|secret|client_secret|api[_-]?secret|signing[_-]?secret)\b\s*[:=]\s*['"`][^'"`\s][^'"`]{5,}['"`]/gi},{id:"SECRET014",type:"Hardcoded API Key Assignment",severity:"high",recommendation:"Move API keys to a cloud secret manager and reference them through environment variables.",pattern:/\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|token)\b\s*[:=]\s*['"`][A-Za-z0-9_\-./+=]{12,}['"`]/gi},{id:"SECRET015",type:".env File Present",severity:"medium",recommendation:"Ensure .env files are excluded from git and move production secrets to a cloud secret manager.",filePattern:/(^|[\\/])\.env(\.[^\\/]+)?$/i},{id:"SECRET016",type:"LiveKit API Secret",severity:"critical",recommendation:"Rotate the LiveKit API secret and store it in managed secrets.",pattern:/\b(?:LIVEKIT_API_SECRET|livekit[_-]?api[_-]?secret)\b\s*[:=]\s*['"`]?([A-Za-z0-9]{24,})['"`]?/gi}]}async scanAll(){const e=Date.now(),t=await this._discoverFiles(this.config.rootPath),i=[];for(const e of t)i.push(...await this.scanFile(e));return this.config.includeGitHistory&&i.push(...this.scanGitHistory()),{filesScanned:t.length,issues:i,summary:this._summarizeIssues(i),duration:Date.now()-e}}async scanFiles(e){const t=[];for(const i of e)t.push(...await this.scanFile(i));return this.config.includeGitHistory&&t.push(...this.scanGitHistory()),{filesScanned:e.length,issues:t,summary:this._summarizeIssues(t)}}async scanFile(e){const t=[],i=path.relative(this.config.rootPath,e).replace(/\\/g,"/");try{if(fs.statSync(e).size>this.config.maxFileSize)return t;const s=path.basename(e),n=fs.readFileSync(e,"utf8").split(/\r?\n/);for(const r of this.secretPatterns)if(r.filePattern&&r.filePattern.test(e)){if(this._shouldSkipLine(s,i,r))continue;this._isGitIgnored(i)||t.push(this._createIssue({file:e,line:1,type:r.type,severity:r.severity,recommendation:"Add this file to .gitignore and move any production secrets to a cloud secret manager.",id:r.id,match:s,category:"secret"}))}else if(r.pattern)for(let s=0;s<n.length;s++){const o=n[s];if(this._shouldSkipLine(o,i,r))continue;let a;for(r.pattern.lastIndex=0;null!==(a=r.pattern.exec(o));){const i=a[1]||a[0];this._isLikelyPlaceholder(i)||t.push(this._createIssue({file:e,line:s+1,column:a.index,type:r.type,severity:r.severity,recommendation:r.recommendation,id:r.id,match:a[0],category:"secret"}))}}return t.push(...this._scanEntropy(e,n,i)),this._dedupeIssues(t)}catch(t){return[{file:e,line:1,category:"secret",severity:"medium",id:"SECRET_SCAN_ERROR",type:"Secret Scan Error",recommendation:"Verify file permissions and encoding, then rerun the scan.",message:"Failed to scan file for secrets: "+("ENOENT"===t.code?"File not found":"EACCES"===t.code?"Permission denied":"Scan error")}]}}scanGitHistory(){try{const e=execFileSync("git",["log","-p","--all","--full-history","--","."],{cwd:this.config.rootPath,encoding:"utf8",maxBuffer:20971520}),t=[],i=e.split(/\r?\n/);let s=null,n=null;for(const e of i){if(e.startsWith("commit ")){s=e.replace("commit ","").trim();continue}if(e.startsWith("+++ b/")){n=e.replace("+++ b/","").trim();continue}if(!e.startsWith("+")||e.startsWith("+++"))continue;const i=e.slice(1);if(!this._shouldSkipLine(i,n||""))for(const e of this.secretPatterns){if(!e.pattern)continue;let r;for(e.pattern.lastIndex=0;null!==(r=e.pattern.exec(i));){const o=r[1]||r[0];this._isLikelyPlaceholder(o)||t.push({file:n||"unknown",line:null,category:"secret-history",severity:"medium"===e.severity?"high":e.severity,id:`${e.id}_HISTORY`,type:`${e.type} in Git History`,recommendation:"Purge the secret from git history, rotate it, and move it to a cloud secret manager.",message:`Potential secret remains in git history (${s||"unknown commit"})`,commit:s,code:i.trim().slice(0,140)})}}}return this._dedupeIssues(t)}catch(e){return[]}}_scanEntropy(e,t,i){const s=[],n=/['"`]([A-Za-z0-9+/=_\-]{20,})['"`]/g;for(let r=0;r<t.length;r++){const o=t[r];if(this._shouldSkipLine(o,i))continue;let a;for(n.lastIndex=0;null!==(a=n.exec(o));){const t=a[1];if(t.length<this.config.minEntropyLength)continue;if(!/[A-Z]/.test(t)||!/[a-z]/.test(t)||!/[0-9]/.test(t))continue;const i=this._calculateEntropy(t);i>=this.config.entropyThreshold&&s.push(this._createIssue({file:e,line:r+1,column:a.index,type:"High Entropy Secret Candidate",severity:"medium",recommendation:"Review this value, move it to a cloud secret manager if it is a credential, and reference it via environment variables.",id:"SECRET_ENTROPY",match:t,category:"secret",extraMessage:`High-entropy string detected (entropy ${i.toFixed(2)})`}))}}return s}_createIssue({file:e,line:t,column:i,type:s,severity:n,recommendation:r,id:o,match:a,category:c,extraMessage:l}){return{file:e,line:t,column:i,category:c,severity:n,id:o,type:s,recommendation:r,message:l||`${s} detected`,code:"string"==typeof a?a.slice(0,140):""}}async _discoverFiles(e,t=[]){let i=[];try{i=fs.readdirSync(e,{withFileTypes:!0})}catch{return t}for(const s of i){const i=path.join(e,s.name),n=path.relative(this.config.rootPath,i).replace(/\\/g,"/");if(this._shouldIgnore(n))continue;if(s.isDirectory()){await this._discoverFiles(i,t);continue}if(!s.isFile())continue;const r=path.extname(s.name).toLowerCase();(this.scanExtensions.has(r)||s.name.startsWith(".env"))&&t.push(i)}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),i=this.ignoredDirectories;for(const e of i)if(t===e||t.startsWith(`${e}/`)||t.includes(`/${e}/`))return!0;return this.config.ignorePatterns.some(e=>{const i=e.replace(/\\/g,"/"),s=i.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");return new RegExp(`^${s}$`).test(t)||t.startsWith(i.replace("/**",""))})}_shouldSkipLine(e,t,i=null){if(!e||!e.trim())return!0;const s=(t||"").replace(/\\/g,"/").toLowerCase();return!!(s.includes("__tests__")||s.includes("/test/")||s.includes("/tests/")||s.endsWith(".spec.js")||s.endsWith(".test.js")||s.endsWith(".spec.ts")||s.endsWith(".test.ts")||s.endsWith(".example")||s.endsWith(".env.example"))||(!i||"SECRET015"!==i.id)&&this.safeLinePatterns.some(t=>t.test(e))}_calculateEntropy(e){const t=new Map;for(const i of e)t.set(i,(t.get(i)||0)+1);let i=0;for(const s of t.values()){const t=s/e.length;i-=t*Math.log2(t)}return i}_isLikelyPlaceholder(e){return!!e&&[/replace[_-\s]?me/i,/replace[_-\s]?with/i,/example/i,/dummy/i,/placeholder/i,/changeme/i,/your[_-\s]?(key|secret|token|password)/i,/local-dev-token/i,/x{8,}/i].some(t=>t.test(e))}_isGitIgnored(e){try{return execFileSync("git",["check-ignore",e],{cwd:this.config.rootPath,stdio:"ignore"}),!0}catch{return!1}}_dedupeIssues(e){const t=new Set;return e.filter(e=>{const i=crypto.createHash("sha1").update(JSON.stringify([e.file,e.line,e.id,e.code,e.commit||""])).digest("hex");return!t.has(i)&&(t.add(i),!0)})}_summarizeIssues(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,high:e.filter(e=>"high"===e.severity).length,medium:e.filter(e=>"medium"===e.severity).length},byType:e.reduce((e,t)=>(e[t.type]=(e[t.type]||0)+1,e),{})}}}module.exports=SecretScanner;
1
+ const fs=require("fs"),path=require("path"),crypto=require("crypto"),EventEmitter=require("events"),{execFileSync:execFileSync}=require("child_process");class SecretScanner extends EventEmitter{constructor(e={}){super();const t=new Set(["rootPath","ignorePatterns","maxFileSize","entropyThreshold","minEntropyLength","includeGitHistory"]),i={};for(const s of Object.keys(e))t.has(s)&&(i[s]=e[s]);this.config={rootPath:i.rootPath||process.cwd(),ignorePatterns:i.ignorePatterns||["node_modules/**",".git/**","dist/**",".next/**","coverage/**"],maxFileSize:i.maxFileSize||1048576,entropyThreshold:i.entropyThreshold||4.2,minEntropyLength:i.minEntropyLength||20,includeGitHistory:void 0===i.includeGitHistory||i.includeGitHistory},this.scanExtensions=new Set([".env",".js",".jsx",".ts",".tsx",".py",".json",".yaml",".yml",".toml",".ini",".conf",".config",".properties",".sh",".bash",".zsh"]),this.ignoredDirectories=["node_modules",".git","dist",".next","coverage",".venv","venv","__pycache__"],this.safeLinePatterns=[/process\.env\./i,/process\.env\[/i,/os\.environ/i,/sample/i,/placeholder/i,/dummy/i,/mock/i,/example/i,/replace[_-\s]?me/i,/replace[_-\s]?with/i,/local-dev-token/i,/changeme/i,/your[_-\s]?key/i,/your[_-\s]?(secret|token|password)/i,/<secret>/i,/<token>/i,/x{8,}/i],this.secretPatterns=[{id:"SECRET001",type:"Stripe Secret Key",severity:"critical",recommendation:"Move this key to a cloud secret manager and load it via environment variables.",pattern:/\bsk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET002",type:"Stripe Publishable Key",severity:"high",recommendation:"Verify this publishable key is intended to be public; if not, move it to managed secrets.",pattern:/\bpk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET003",type:"AWS Access Key",severity:"critical",recommendation:"Rotate the AWS credential immediately and store replacements in a cloud secret manager.",pattern:/\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g},{id:"SECRET004",type:"GitHub Token",severity:"critical",recommendation:"Revoke the GitHub token and replace it with a managed secret or GitHub App credential.",pattern:/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g},{id:"SECRET005",type:"Slack Token",severity:"critical",recommendation:"Rotate the Slack token and move it into a cloud secret manager.",pattern:/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g},{id:"SECRET006",type:"Twilio Credential",severity:"critical",recommendation:"Rotate the Twilio credential and store it in managed secrets.",pattern:/\b(SK|AC)[0-9a-fA-F]{32}\b/g},{id:"SECRET007",type:"Google API Key",severity:"critical",recommendation:"Restrict and rotate the Google API key, then move it to a cloud secret manager.",pattern:/\bAIza[0-9A-Za-z\-_]{35}\b/g},{id:"SECRET007B",type:"OpenAI Project Key",severity:"critical",recommendation:"Rotate the OpenAI project key and move it to a cloud secret manager.",pattern:/\bsk-proj-[A-Za-z0-9\-_]{20,}\b/g},{id:"SECRET007C",type:"Cartesia API Key",severity:"critical",recommendation:"Rotate the Cartesia API key and move it to a cloud secret manager.",pattern:/\bsk_car_[A-Za-z0-9]{16,}\b/g},{id:"SECRET008",type:"JWT Token",severity:"high",recommendation:"Do not hardcode JWTs; issue them dynamically and store signing secrets in managed secrets.",pattern:/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9._-]{8,}\.[A-Za-z0-9._-]{8,}\b/g},{id:"SECRET009",type:"Bearer Token",severity:"high",recommendation:"Move bearer tokens to a cloud secret manager and inject them at runtime.",pattern:/\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/g},{id:"SECRET010",type:"Private Key",severity:"critical",recommendation:"Remove the private key from source control, rotate it, and store it in a secret manager.",pattern:/-----BEGIN (RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g},{id:"SECRET011",type:"Database Connection String with Password",severity:"critical",recommendation:"Move the connection string to managed secrets and avoid embedding passwords in code or config.",pattern:/\b(?:postgres(?:ql)?|mysql|mssql|mongodb(?:\+srv)?|redis):\/\/[^:\s]+:[^@\s]+@[^'"`\s]+\b/gi},{id:"SECRET012",type:"Webhook Secret",severity:"high",recommendation:"Store webhook secrets in a cloud secret manager and inject them via environment variables.",pattern:/\b(?:whsec_[A-Za-z0-9]+|webhook[_-]?secret[_:=\s'"]+[A-Za-z0-9_\-]{12,})\b/gi},{id:"SECRET013",type:"Hardcoded Password or Secret Assignment",severity:"high",recommendation:"Replace hardcoded credentials with environment variables backed by a cloud secret manager.",pattern:/\b(?:password|passwd|pwd|secret|client_secret|api[_-]?secret|signing[_-]?secret)\b\s*[:=]\s*['"`][^'"`\s][^'"`]{5,}['"`]/gi},{id:"SECRET014",type:"Hardcoded API Key Assignment",severity:"high",recommendation:"Move API keys to a cloud secret manager and reference them through environment variables.",pattern:/\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|token)\b\s*[:=]\s*['"`][A-Za-z0-9_\-./+=]{12,}['"`]/gi},{id:"SECRET015",type:".env File Present",severity:"medium",recommendation:"Ensure .env files are excluded from git and move production secrets to a cloud secret manager.",filePattern:/(^|[\\/])\.env(\.[^\\/]+)?$/i},{id:"SECRET016",type:"LiveKit API Secret",severity:"critical",recommendation:"Rotate the LiveKit API secret and store it in managed secrets.",pattern:/\b(?:LIVEKIT_API_SECRET|livekit[_-]?api[_-]?secret)\b\s*[:=]\s*['"`]?([A-Za-z0-9]{24,})['"`]?/gi}]}async scanAll(){const e=Date.now(),t=await this._discoverFiles(this.config.rootPath),i=[];for(const e of t)i.push(...await this.scanFile(e));return this.config.includeGitHistory&&i.push(...this.scanGitHistory()),{filesScanned:t.length,issues:i,summary:this._summarizeIssues(i),duration:Date.now()-e}}async scanFiles(e){const t=[];for(const i of e)t.push(...await this.scanFile(i));return this.config.includeGitHistory&&t.push(...this.scanGitHistory()),{filesScanned:e.length,issues:t,summary:this._summarizeIssues(t)}}async scanFile(e){const t=[],i=path.relative(this.config.rootPath,e).replace(/\\/g,"/");try{if(fs.statSync(e).size>this.config.maxFileSize)return t;const s=path.basename(e),n=fs.readFileSync(e,"utf8").split(/\r?\n/);for(const r of this.secretPatterns)if(r.filePattern&&r.filePattern.test(e)){if(this._shouldSkipLine(s,i,r))continue;this._isGitIgnored(i)||t.push(this._createIssue({file:e,line:1,type:r.type,severity:r.severity,recommendation:"Add this file to .gitignore and move any production secrets to a cloud secret manager.",id:r.id,match:s,category:"secret"}))}else if(r.pattern)for(let s=0;s<n.length;s++){const o=n[s];if(this._shouldSkipLine(o,i,r))continue;let a;for(r.pattern.lastIndex=0;null!==(a=r.pattern.exec(o));){const i=a[1]||a[0];this._isLikelyPlaceholder(i)||t.push(this._createIssue({file:e,line:s+1,column:a.index,type:r.type,severity:r.severity,recommendation:r.recommendation,id:r.id,match:a[0],category:"secret"}))}}return t.push(...this._scanEntropy(e,n,i)),this._dedupeIssues(t)}catch(t){return[{file:e,line:1,category:"secret",severity:"medium",id:"SECRET_SCAN_ERROR",type:"Secret Scan Error",recommendation:"Verify file permissions and encoding, then rerun the scan.",message:"Failed to scan file for secrets: "+("ENOENT"===t.code?"File not found":"EACCES"===t.code?"Permission denied":"Scan error")}]}}scanGitHistory(){try{const e=execFileSync("git",["log","-p","--all","--full-history","--","."],{cwd:this.config.rootPath,encoding:"utf8",maxBuffer:20971520}),t=[],i=e.split(/\r?\n/);let s=null,n=null;for(const e of i){if(e.startsWith("commit ")){s=e.replace("commit ","").trim();continue}if(e.startsWith("+++ b/")){n=e.replace("+++ b/","").trim();continue}if(!e.startsWith("+")||e.startsWith("+++"))continue;const i=e.slice(1);if(!this._shouldSkipLine(i,n||""))for(const e of this.secretPatterns){if(!e.pattern)continue;let r;for(e.pattern.lastIndex=0;null!==(r=e.pattern.exec(i));){const o=r[1]||r[0];this._isLikelyPlaceholder(o)||t.push({file:n||"unknown",line:null,category:"secret-history",severity:"medium"===e.severity?"high":e.severity,id:`${e.id}_HISTORY`,type:`${e.type} in Git History`,recommendation:"Purge the secret from git history, rotate it, and move it to a cloud secret manager.",message:`Potential secret remains in git history (${s||"unknown commit"})`,commit:s,code:i.trim().slice(0,140)})}}}return this._dedupeIssues(t)}catch(e){return[]}}_scanEntropy(e,t,i){const s=[],n=/['"`]([A-Za-z0-9+/=_\-]{20,})['"`]/g;for(let r=0;r<t.length;r++){const o=t[r];if(this._shouldSkipLine(o,i))continue;let a;for(n.lastIndex=0;null!==(a=n.exec(o));){const t=a[1];if(t.length<this.config.minEntropyLength)continue;if(!/[A-Z]/.test(t)||!/[a-z]/.test(t)||!/[0-9]/.test(t))continue;const i=this._calculateEntropy(t);i>=this.config.entropyThreshold&&s.push(this._createIssue({file:e,line:r+1,column:a.index,type:"High Entropy Secret Candidate",severity:"medium",recommendation:"Review this value, move it to a cloud secret manager if it is a credential, and reference it via environment variables.",id:"SECRET_ENTROPY",match:t,category:"secret",extraMessage:`High-entropy string detected (entropy ${i.toFixed(2)})`}))}}return s}_createIssue({file:e,line:t,column:i,type:s,severity:n,recommendation:r,id:o,match:a,category:c,extraMessage:l}){return{file:e,line:t,column:i,category:c,severity:n,id:o,type:s,recommendation:r,message:l||`${s} detected`,code:"string"==typeof a?a.slice(0,140):""}}async _discoverFiles(e,t=[]){let i=[];try{i=fs.readdirSync(e,{withFileTypes:!0})}catch{return t}for(const s of i){const i=path.join(e,s.name),n=path.relative(this.config.rootPath,i).replace(/\\/g,"/");if(this._shouldIgnore(n))continue;if(s.isDirectory()){await this._discoverFiles(i,t);continue}if(!s.isFile())continue;const r=path.extname(s.name).toLowerCase();(this.scanExtensions.has(r)||s.name.startsWith(".env"))&&t.push(i)}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),i=this.ignoredDirectories;for(const e of i)if(t===e||t.startsWith(`${e}/`)||t.includes(`/${e}/`))return!0;return this.config.ignorePatterns.some(e=>{const i=e.replace(/\\/g,"/"),s=i.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");return new RegExp(`^${s}$`).test(t)||t.startsWith(i.replace("/**",""))})}_shouldSkipLine(e,t,i=null){if(!e||!e.trim())return!0;const s=(t||"").replace(/\\/g,"/").toLowerCase();return!!(s.includes("__tests__")||s.includes("/test/")||s.includes("/tests/")||s.endsWith(".spec.js")||s.endsWith(".test.js")||s.endsWith(".spec.ts")||s.endsWith(".test.ts")||s.endsWith(".example")||s.endsWith(".env.example"))||(!i||"SECRET015"!==i.id)&&this.safeLinePatterns.some(t=>t.test(e))}_calculateEntropy(e){const t=new Map;for(const i of e)t.set(i,(t.get(i)||0)+1);let i=0;for(const s of t.values()){const t=s/e.length;i-=t*Math.log2(t)}return i}_isLikelyPlaceholder(e){return!!e&&[/replace[_-\s]?me/i,/replace[_-\s]?with/i,/example/i,/dummy/i,/placeholder/i,/changeme/i,/your[_-\s]?(key|secret|token|password)/i,/local-dev-token/i,/x{8,}/i].some(t=>t.test(e))}_isGitIgnored(e){try{return execFileSync("git",["check-ignore",e],{cwd:this.config.rootPath,stdio:"ignore"}),!0}catch{return!1}}_dedupeIssues(e){const t=new Set;return e.filter(e=>{const i=crypto.createHash("sha1").update(JSON.stringify([e.file,e.line,e.id,e.code,e.commit||""])).digest("hex");return!t.has(i)&&(t.add(i),!0)})}_summarizeIssues(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,high:e.filter(e=>"high"===e.severity).length,medium:e.filter(e=>"medium"===e.severity).length},byType:e.reduce((e,t)=>(e[t.type]=(e[t.type]||0)+1,e),{})}}}module.exports=SecretScanner;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.4.0",
4
- "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Zero dependencies.",
3
+ "version": "0.4.2",
4
+ "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Minimal dependencies.",
5
5
  "bin": {
6
6
  "thuban": "./dist/cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "scan": "node cli.js scan",
10
- "test": "node cli.js scan . --json > /dev/null",
10
+ "test": "node cli.js scan . --json > NUL",
11
11
  "build": "node build.js",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
@@ -48,10 +48,13 @@
48
48
  "dist/packages/crucible/*.js",
49
49
  "dist/packages/crucible/seeds/**",
50
50
  "dist/packages/crucible/.golden/",
51
+ "dist/packages/crucible/rules/**",
51
52
  "dist/README.md",
52
- "dist/LICENSE"
53
+ "dist/LICENSE",
54
+ "dist/package.json"
53
55
  ],
54
56
  "dependencies": {
57
+ "@babel/parser": "^8.0.0",
55
58
  "docx": "^9.7.1"
56
59
  },
57
60
  "devDependencies": {
@@ -1,37 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: xss
3
- # severity: high
4
- # language: python
5
- # expected: XSS via Jinja2 |safe filter applied to user-controlled content
6
-
7
- from flask import Flask, request, render_template_string
8
-
9
- app = Flask(__name__)
10
-
11
- ARTICLE_TEMPLATE = """
12
- <!DOCTYPE html>
13
- <html>
14
- <head><title>{{ title }}</title></head>
15
- <body>
16
- <article>
17
- <h1>{{ title }}</h1>
18
- <!-- |safe disables Jinja2 autoescaping — XSS if content is user-controlled -->
19
- <div class="content">{{ content | safe }}</div>
20
- <footer>By: {{ author | safe }}</footer>
21
- </article>
22
- </body>
23
- </html>
24
- """
25
-
26
-
27
- @app.route("/articles/<int:article_id>")
28
- def view_article(article_id: int):
29
- # In practice, content and author come from DB — but users control their own author field
30
- from myapp.models import Article
31
- article = Article.objects.get(pk=article_id)
32
- return render_template_string(
33
- ARTICLE_TEMPLATE,
34
- title=article.title,
35
- content=article.body, # user-submitted rich text
36
- author=article.author_bio, # user-controlled bio
37
- )
@@ -1,34 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: xss
3
- # severity: medium
4
- # language: python
5
- # expected: XSS via FastAPI response with HTML and user-controlled query parameter
6
-
7
- from fastapi import FastAPI, Query
8
- from fastapi.responses import HTMLResponse
9
-
10
- app = FastAPI()
11
-
12
-
13
- @app.get("/welcome", response_class=HTMLResponse)
14
- async def welcome(name: str = Query(default="Guest")):
15
- # name injected directly into HTML — no escaping applied
16
- html = f"""
17
- <!DOCTYPE html>
18
- <html>
19
- <head><title>Welcome</title></head>
20
- <body>
21
- <h1>Hello, {name}!</h1>
22
- <p>Thanks for visiting.</p>
23
- </body>
24
- </html>
25
- """
26
- return HTMLResponse(content=html)
27
-
28
-
29
- @app.get("/redirect-info", response_class=HTMLResponse)
30
- async def redirect_info(from_url: str = Query(default="/")):
31
- # from_url reflected in response — open redirect + XSS
32
- return HTMLResponse(
33
- f'<p>You were redirected from: <a href="{from_url}">{from_url}</a></p>'
34
- )