thuban 0.4.12 → 0.4.13

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 (184) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/packages/crucible/rules/code-scanner-core.json +11 -0
  4. package/dist/packages/scanner/ghost-code-detector.js +1 -1
  5. package/package.json +1 -1
  6. package/dist/packages/crucible/seeds/python/seed-021 2.py +0 -37
  7. package/dist/packages/crucible/seeds/python/seed-022 2.py +0 -34
  8. package/dist/packages/crucible/seeds/python/seed-023 2.py +0 -37
  9. package/dist/packages/crucible/seeds/python/seed-024 2.py +0 -38
  10. package/dist/packages/crucible/seeds/python/seed-025 2.py +0 -40
  11. package/dist/packages/crucible/seeds/python/seed-026 2.py +0 -35
  12. package/dist/packages/crucible/seeds/python/seed-027 2.py +0 -35
  13. package/dist/packages/crucible/seeds/python/seed-028 2.py +0 -42
  14. package/dist/packages/crucible/seeds/python/seed-030 2.py +0 -37
  15. package/dist/packages/crucible/seeds/python/seed-031 2.py +0 -34
  16. package/dist/packages/crucible/seeds/python/seed-036 2.py +0 -33
  17. package/dist/packages/crucible/seeds/python/seed-037 2.py +0 -41
  18. package/dist/packages/crucible/seeds/python/seed-038 2.py +0 -33
  19. package/dist/packages/crucible/seeds/python/seed-039 2.py +0 -39
  20. package/dist/packages/crucible/seeds/python/seed-040 2.py +0 -39
  21. package/dist/packages/crucible/seeds/python/seed-041 2.py +0 -37
  22. package/dist/packages/crucible/seeds/python/seed-042 2.py +0 -38
  23. package/dist/packages/crucible/seeds/python/seed-043 2.py +0 -32
  24. package/dist/packages/crucible/seeds/python/seed-044 2.py +0 -38
  25. package/dist/packages/crucible/seeds/python/seed-045 2.py +0 -36
  26. package/dist/packages/crucible/seeds/python/seed-046 2.py +0 -33
  27. package/dist/packages/crucible/seeds/python/seed-047 2.py +0 -44
  28. package/dist/packages/crucible/seeds/python/seed-048 2.py +0 -35
  29. package/dist/packages/crucible/seeds/python/seed-049 2.py +0 -39
  30. package/dist/packages/crucible/seeds/python/seed-050 2.py +0 -39
  31. package/dist/packages/crucible/seeds/python/seed-051 2.py +0 -38
  32. package/dist/packages/crucible/seeds/python/seed-052 2.py +0 -41
  33. package/dist/packages/crucible/seeds/ruby/seed-001 2.rb +0 -15
  34. package/dist/packages/crucible/seeds/ruby/seed-002 2.rb +0 -22
  35. package/dist/packages/crucible/seeds/ruby/seed-003 2.rb +0 -25
  36. package/dist/packages/crucible/seeds/ruby/seed-004 2.rb +0 -17
  37. package/dist/packages/crucible/seeds/ruby/seed-005 2.rb +0 -21
  38. package/dist/packages/crucible/seeds/ruby/seed-006 2.rb +0 -17
  39. package/dist/packages/crucible/seeds/ruby/seed-007 2.rb +0 -16
  40. package/dist/packages/crucible/seeds/ruby/seed-008 2.rb +0 -18
  41. package/dist/packages/crucible/seeds/ruby/seed-009 2.rb +0 -20
  42. package/dist/packages/crucible/seeds/ruby/seed-010 2.rb +0 -24
  43. package/dist/packages/crucible/seeds/ruby/seed-011 2.rb +0 -21
  44. package/dist/packages/crucible/seeds/ruby/seed-012 2.rb +0 -22
  45. package/dist/packages/crucible/seeds/ruby/seed-013 2.rb +0 -21
  46. package/dist/packages/crucible/seeds/ruby/seed-014 2.rb +0 -16
  47. package/dist/packages/crucible/seeds/ruby/seed-015 2.rb +0 -18
  48. package/dist/packages/crucible/seeds/ruby/seed-016 2.rb +0 -17
  49. package/dist/packages/crucible/seeds/ruby/seed-017 2.rb +0 -25
  50. package/dist/packages/crucible/seeds/ruby/seed-018 2.rb +0 -23
  51. package/dist/packages/crucible/seeds/ruby/seed-019 2.rb +0 -20
  52. package/dist/packages/crucible/seeds/ruby/seed-020 2.rb +0 -17
  53. package/dist/packages/crucible/seeds/ruby/seed-021 2.rb +0 -20
  54. package/dist/packages/crucible/seeds/ruby/seed-022 2.rb +0 -21
  55. package/dist/packages/crucible/seeds/ruby/seed-023 2.rb +0 -19
  56. package/dist/packages/crucible/seeds/ruby/seed-024 2.rb +0 -17
  57. package/dist/packages/crucible/seeds/ruby/seed-025 2.rb +0 -17
  58. package/dist/packages/crucible/seeds/ruby/seed-026 2.rb +0 -18
  59. package/dist/packages/crucible/seeds/ruby/seed-027 2.rb +0 -21
  60. package/dist/packages/crucible/seeds/ruby/seed-028 2.rb +0 -22
  61. package/dist/packages/crucible/seeds/ruby/seed-029 2.rb +0 -19
  62. package/dist/packages/crucible/seeds/ruby/seed-030 2.rb +0 -20
  63. package/dist/packages/crucible/seeds/ruby/seed-031 2.rb +0 -17
  64. package/dist/packages/crucible/seeds/ruby/seed-032 2.rb +0 -22
  65. package/dist/packages/crucible/seeds/ruby/seed-033 2.rb +0 -18
  66. package/dist/packages/crucible/seeds/ruby/seed-034 2.rb +0 -19
  67. package/dist/packages/crucible/seeds/ruby/seed-035 2.rb +0 -19
  68. package/dist/packages/crucible/seeds/ruby/seed-036 2.rb +0 -18
  69. package/dist/packages/crucible/seeds/ruby/seed-037 2.rb +0 -21
  70. package/dist/packages/crucible/seeds/ruby/seed-038 2.rb +0 -24
  71. package/dist/packages/crucible/seeds/ruby/seed-039 2.rb +0 -24
  72. package/dist/packages/crucible/seeds/ruby/seed-040 2.rb +0 -22
  73. package/dist/packages/crucible/seeds/ruby/seed-041 2.rb +0 -23
  74. package/dist/packages/crucible/seeds/ruby/seed-042 2.rb +0 -25
  75. package/dist/packages/crucible/seeds/ruby/seed-043 2.rb +0 -23
  76. package/dist/packages/crucible/seeds/ruby/seed-044 2.rb +0 -16
  77. package/dist/packages/crucible/seeds/ruby/seed-045 2.rb +0 -22
  78. package/dist/packages/crucible/seeds/ruby/seed-046 2.rb +0 -27
  79. package/dist/packages/crucible/seeds/ruby/seed-047 2.rb +0 -26
  80. package/dist/packages/crucible/seeds/ruby/seed-048 2.rb +0 -24
  81. package/dist/packages/crucible/seeds/ruby/seed-049 2.rb +0 -20
  82. package/dist/packages/crucible/seeds/ruby/seed-050 2.rb +0 -27
  83. package/dist/packages/crucible/seeds/rust/seed-001 2.rs +0 -25
  84. package/dist/packages/crucible/seeds/rust/seed-002 2.rs +0 -20
  85. package/dist/packages/crucible/seeds/rust/seed-003 2.rs +0 -23
  86. package/dist/packages/crucible/seeds/rust/seed-004 2.rs +0 -21
  87. package/dist/packages/crucible/seeds/rust/seed-005 2.rs +0 -21
  88. package/dist/packages/crucible/seeds/rust/seed-006 2.rs +0 -24
  89. package/dist/packages/crucible/seeds/rust/seed-007 2.rs +0 -20
  90. package/dist/packages/crucible/seeds/rust/seed-008 2.rs +0 -19
  91. package/dist/packages/crucible/seeds/rust/seed-009 2.rs +0 -28
  92. package/dist/packages/crucible/seeds/rust/seed-010 2.rs +0 -28
  93. package/dist/packages/crucible/seeds/rust/seed-011 2.rs +0 -25
  94. package/dist/packages/crucible/seeds/rust/seed-012 2.rs +0 -31
  95. package/dist/packages/crucible/seeds/rust/seed-013 2.rs +0 -27
  96. package/dist/packages/crucible/seeds/rust/seed-014 2.rs +0 -30
  97. package/dist/packages/crucible/seeds/rust/seed-015 2.rs +0 -33
  98. package/dist/packages/crucible/seeds/rust/seed-016 2.rs +0 -22
  99. package/dist/packages/crucible/seeds/rust/seed-017 2.rs +0 -28
  100. package/dist/packages/crucible/seeds/rust/seed-018 2.rs +0 -21
  101. package/dist/packages/crucible/seeds/rust/seed-019 2.rs +0 -36
  102. package/dist/packages/crucible/seeds/rust/seed-020 2.rs +0 -27
  103. package/dist/packages/crucible/seeds/rust/seed-021 2.rs +0 -26
  104. package/dist/packages/crucible/seeds/rust/seed-022 2.rs +0 -23
  105. package/dist/packages/crucible/seeds/rust/seed-023 2.rs +0 -22
  106. package/dist/packages/crucible/seeds/rust/seed-024 2.rs +0 -24
  107. package/dist/packages/crucible/seeds/rust/seed-025 2.rs +0 -29
  108. package/dist/packages/crucible/seeds/rust/seed-026 2.rs +0 -23
  109. package/dist/packages/crucible/seeds/rust/seed-027 2.rs +0 -24
  110. package/dist/packages/crucible/seeds/rust/seed-028 2.rs +0 -25
  111. package/dist/packages/crucible/seeds/rust/seed-029 2.rs +0 -25
  112. package/dist/packages/crucible/seeds/rust/seed-030 2.rs +0 -30
  113. package/dist/packages/crucible/seeds/rust/seed-031 2.rs +0 -22
  114. package/dist/packages/crucible/seeds/rust/seed-032 2.rs +0 -25
  115. package/dist/packages/crucible/seeds/rust/seed-033 2.rs +0 -25
  116. package/dist/packages/crucible/seeds/rust/seed-034 2.rs +0 -20
  117. package/dist/packages/crucible/seeds/rust/seed-035 2.rs +0 -28
  118. package/dist/packages/crucible/seeds/rust/seed-036 2.rs +0 -26
  119. package/dist/packages/crucible/seeds/rust/seed-037 2.rs +0 -31
  120. package/dist/packages/crucible/seeds/rust/seed-038 2.rs +0 -25
  121. package/dist/packages/crucible/seeds/rust/seed-039 2.rs +0 -28
  122. package/dist/packages/crucible/seeds/rust/seed-040 2.rs +0 -27
  123. package/dist/packages/crucible/seeds/rust/seed-041 2.rs +0 -32
  124. package/dist/packages/crucible/seeds/rust/seed-042 2.rs +0 -27
  125. package/dist/packages/crucible/seeds/rust/seed-043 2.rs +0 -29
  126. package/dist/packages/crucible/seeds/rust/seed-044 2.rs +0 -25
  127. package/dist/packages/crucible/seeds/rust/seed-045 2.rs +0 -28
  128. package/dist/packages/crucible/seeds/rust/seed-046 2.rs +0 -25
  129. package/dist/packages/crucible/seeds/rust/seed-047 2.rs +0 -34
  130. package/dist/packages/crucible/seeds/rust/seed-048 2.rs +0 -21
  131. package/dist/packages/crucible/seeds/rust/seed-049 2.rs +0 -26
  132. package/dist/packages/crucible/seeds/rust/seed-050 2.rs +0 -23
  133. package/dist/packages/crucible/seeds/ts/seed-001 2.ts +0 -32
  134. package/dist/packages/crucible/seeds/ts/seed-002 2.ts +0 -34
  135. package/dist/packages/crucible/seeds/ts/seed-003 2.ts +0 -28
  136. package/dist/packages/crucible/seeds/ts/seed-004 2.ts +0 -34
  137. package/dist/packages/crucible/seeds/ts/seed-005 2.ts +0 -32
  138. package/dist/packages/crucible/seeds/ts/seed-006 2.ts +0 -31
  139. package/dist/packages/crucible/seeds/ts/seed-007 2.ts +0 -28
  140. package/dist/packages/crucible/seeds/ts/seed-008 2.ts +0 -40
  141. package/dist/packages/crucible/seeds/ts/seed-009 2.ts +0 -31
  142. package/dist/packages/crucible/seeds/ts/seed-010 2.ts +0 -33
  143. package/dist/packages/crucible/seeds/ts/seed-011 2.ts +0 -29
  144. package/dist/packages/crucible/seeds/ts/seed-012 2.ts +0 -34
  145. package/dist/packages/crucible/seeds/ts/seed-013 2.ts +0 -31
  146. package/dist/packages/crucible/seeds/ts/seed-014 2.ts +0 -36
  147. package/dist/packages/crucible/seeds/ts/seed-015 2.ts +0 -31
  148. package/dist/packages/crucible/seeds/ts/seed-016 2.ts +0 -37
  149. package/dist/packages/crucible/seeds/ts/seed-017 2.ts +0 -44
  150. package/dist/packages/crucible/seeds/ts/seed-018 2.ts +0 -33
  151. package/dist/packages/crucible/seeds/ts/seed-019 2.ts +0 -32
  152. package/dist/packages/crucible/seeds/ts/seed-020 2.ts +0 -33
  153. package/dist/packages/crucible/seeds/ts/seed-021 2.ts +0 -33
  154. package/dist/packages/crucible/seeds/ts/seed-022 2.ts +0 -34
  155. package/dist/packages/crucible/seeds/ts/seed-023 2.ts +0 -33
  156. package/dist/packages/crucible/seeds/ts/seed-024 2.ts +0 -35
  157. package/dist/packages/crucible/seeds/ts/seed-025 2.ts +0 -29
  158. package/dist/packages/crucible/seeds/ts/seed-026 2.ts +0 -36
  159. package/dist/packages/crucible/seeds/ts/seed-027 2.ts +0 -30
  160. package/dist/packages/crucible/seeds/ts/seed-028 2.ts +0 -32
  161. package/dist/packages/crucible/seeds/ts/seed-029 2.ts +0 -34
  162. package/dist/packages/crucible/seeds/ts/seed-030 2.ts +0 -37
  163. package/dist/packages/crucible/seeds/ts/seed-031 2.ts +0 -31
  164. package/dist/packages/crucible/seeds/ts/seed-032 2.ts +0 -32
  165. package/dist/packages/crucible/seeds/ts/seed-033 2.ts +0 -32
  166. package/dist/packages/crucible/seeds/ts/seed-034 2.ts +0 -34
  167. package/dist/packages/crucible/seeds/ts/seed-035 2.ts +0 -29
  168. package/dist/packages/crucible/seeds/ts/seed-036 2.ts +0 -34
  169. package/dist/packages/crucible/seeds/ts/seed-037 2.ts +0 -33
  170. package/dist/packages/crucible/seeds/ts/seed-038 2.ts +0 -29
  171. package/dist/packages/crucible/seeds/ts/seed-039 2.ts +0 -35
  172. package/dist/packages/crucible/seeds/ts/seed-040 2.ts +0 -31
  173. package/dist/packages/crucible/seeds/ts/seed-041 2.ts +0 -29
  174. package/dist/packages/crucible/seeds/ts/seed-042 2.ts +0 -33
  175. package/dist/packages/crucible/seeds/ts/seed-043 2.ts +0 -32
  176. package/dist/packages/crucible/seeds/ts/seed-044 2.ts +0 -36
  177. package/dist/packages/crucible/seeds/ts/seed-045 2.ts +0 -39
  178. package/dist/packages/crucible/seeds/ts/seed-046 2.ts +0 -41
  179. package/dist/packages/crucible/seeds/ts/seed-047 2.ts +0 -33
  180. package/dist/packages/crucible/seeds/ts/seed-048 2.ts +0 -33
  181. package/dist/packages/crucible/seeds/ts/seed-049 2.ts +0 -34
  182. package/dist/packages/crucible/seeds/ts/seed-050 2.ts +0 -41
  183. package/dist/packages/crucible/seeds/ts/seed-051 2.ts +0 -34
  184. package/dist/packages/crucible/seeds/ts/seed-052 2.ts +0 -36
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
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": "cli.js"
@@ -299,5 +299,16 @@
299
299
  "message": "SQL query built with PHP variable interpolation — use PDO prepared statements",
300
300
  "owasp": "A03:2021 Injection",
301
301
  "cwe": "CWE-89"
302
+ },
303
+ {
304
+ "id": "SEC022",
305
+ "name": "Rust Shell Command Injection (Command::new)",
306
+ "type": "command_injection",
307
+ "pattern": "Command::new\\s*\\(\\s*[\"'](?:/bin/)?(?:sh|bash|zsh)[\"']\\s*\\)",
308
+ "flags": "",
309
+ "severity": "critical",
310
+ "message": "Command::new spawns a shell (sh/bash) — arguments built with format!()/user input risk command injection; avoid shell invocation, pass args directly to Command",
311
+ "owasp": "A03:2021 Injection",
312
+ "cwe": "CWE-78"
302
313
  }
303
314
  ]
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path");class GhostCodeDetector{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}scan(e){const t=[],s={},n=new Map,i=new Map,o=new Map;for(const r of e){let e;try{if(fs.statSync(r).size>524288)continue;e=fs.readFileSync(r,"utf-8").replace(/\r\n/g,"\n")}catch(e){continue}const c=path.relative(this.rootPath,r);if(this._isSkippable(c)){o.set(c,e);continue}const a=e.split("\n");n.set(c,e),i.set(c,a);const l=path.extname(r).toLowerCase(),f=this._computeTemplateLiteralLines(e);for(let e=0;e<a.length;e++){if(f.has(e))continue;const n=a[e],i=this._extractFunction(n,e,a,l);if(i){i.file=c,i.line=e+1,i.lineCount=this._countFunctionLines(a,e,l),i.declKind="function",i.isExport&&this._isFrameworkEntryFile(c)&&(i.isLifecycle=!0),t.push(i),s[i.name]||(s[i.name]=0);continue}const o=this._extractClass(n);if(o){o.file=c,o.line=e+1,o.lineCount=this._countFunctionLines(a,e,l),o.declKind="class",t.push(o),s[o.name]||(s[o.name]=0);continue}const r=this._extractConstDeclaration(n);if(r){r.file=c,r.line=e+1,r.lineCount=1,r.declKind="const",t.push(r),s[r.name]||(s[r.name]=0);continue}const u=this._extractImports(n);for(const n of u)n.file=c,n.line=e+1,n.lineCount=1,n.declKind="import",t.push(n),s[n.name]||(s[n.name]=0)}}const r=new Map([...n,...o]);for(const[e,n]of r){const i=this._stripCommentsAndStrings(n);for(const n of t){const t=new RegExp(`\\b${this._escapeRegex(n.name)}\\b`,"g"),o=i.match(t);if(o){const t=e===n.file;s[n.name]+=o.length-(t?1:0)}}}const c=t.filter(e=>"function"===e.declKind).filter(e=>(s[e.name]||0)<=0&&!e.isLifecycle&&e.lineCount>=3).map(e=>({...e,references:s[e.name]||0,wastedLines:e.lineCount,category:"ghost",severity:e.isExport?"low":e.lineCount>30?"high":"medium",message:`${e.name}() — ${e.lineCount} lines, never called${e.isExport?" (exported — verify no external consumers)":""}`})),a=(e,n,i,o)=>t.filter(t=>t.declKind===e).filter(e=>(s[e.name]||0)<=0).map(e=>({...e,references:s[e.name]||0,wastedLines:e.lineCount,category:n,severity:i(e),message:o(e)})),l=a("class","ghost_class",e=>e.isExport?"low":"medium",e=>`class ${e.name} — ${e.lineCount} lines, never referenced${e.isExport?" (exported — verify no external consumers)":""}`),f=a("const","ghost_const",e=>e.isExport?"low":"medium",e=>`${e.kind} ${e.name} — declared but never used${e.isExport?" (exported — verify no external consumers)":""}`),u=a("import","ghost_import",()=>"low",e=>`import '${e.name}' from '${e.source}' — never used`),p=this._findDeadClusters(t.filter(e=>"function"===e.declKind),n,s),m=[...c,...l,...f,...u,...p].sort((e,t)=>t.wastedLines-e.wastedLines),h=m.reduce((e,t)=>e+t.wastedLines,0),d=e.reduce((e,t)=>{try{return e+fs.readFileSync(t,"utf-8").replace(/\r\n/g,"\n").split("\n").length}catch(t){return e}},0);return{ghosts:m,totalGhosts:m.length,totalWastedLines:h,wastedPercent:d>0?Math.min(100,Math.round(h/d*1e3)/10):0,totalLines:d,breakdown:{functions:c.length,classes:l.length,consts:f.length,imports:u.length,deadClusters:p.length}}}_extractFunction(e,t,s,n){const i=e.trim(),o=new Set([".py",".pyw",".pyi",".pyx"]),r=n&&o.has(n),c=i.replace(/^export\s+(?:default\s+)?/,"");let a=c.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);return a?{name:a[1],type:"function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:(a=c.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/),a?{name:a[1],type:"const-function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:(a=c.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/),a&&(c.includes("=>")||t+1<s.length&&s[t+1].includes("=>"))?{name:a[1],type:"arrow",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:r&&(a=i.match(/^(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(\[]/),a)?{name:a[1],type:"python-def",isExport:!/^_/.test(a[1]),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),a?{name:a[1],type:"kotlin-fun",isExport:!i.startsWith("private"),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),a?{name:a[1],type:"go-func",isExport:/^[A-Z]/.test(a[1]),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),a?{name:a[1],type:"rust-fn",isExport:/^pub/.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),a?{name:a[1],type:"php-function",isExport:!/private/.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),a?{name:a[1],type:"ruby-def",isExport:/^def\s+self\./.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/),a&&!["if","for","while","switch","catch","constructor","render","toString"].includes(a[1])?{name:a[1],type:"method",isExport:!1,isLifecycle:this._isLifecycle(a[1])}:null))))))))}_isExported(e,t,s){return!!/^export\s/.test(e.trim())||!!/module\.exports/.test(e)}_extractClass(e){const t=e.trim(),s=t.match(/^(?:export\s+(?:default\s+)?)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return s?{name:s[1],type:"class",isExport:/^export\s/.test(t),isLifecycle:!1}:null}_extractConstDeclaration(e){const t=e.trim().match(/^(export\s+)?(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)$/);if(!t)return null;const[,s,n,i,o]=t;return/^(?:async\s+)?function\b/.test(o)||/=>/.test(o)||""===o.trim()||/^\(/.test(o.trim())&&/=>/.test(o)||/require\s*\(/.test(o)?null:{name:i,kind:n,type:"const-declaration",isExport:!!s,isLifecycle:!1}}_extractImports(e){const t=e.trim(),s=[];let n=t.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);if(n){const e=n[1].split(",").map(e=>e.trim()).filter(Boolean);for(const t of e){const e=t.split(/\s+as\s+/),i=(e[1]||e[0]).trim();i&&s.push({name:i,source:n[2],type:"import"})}return s}if(n=t.match(/^import\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:,|from)/),n&&!/^import\s*\{/.test(t)){const e=t.match(/from\s*['"]([^'"]+)['"]/);return s.push({name:n[1],source:e?e[1]:"",type:"import"}),s}if(n=t.match(/^import\s*\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*from\s*['"]([^'"]+)['"]/),n)return s.push({name:n[1],source:n[2],type:"import"}),s;if(n=t.match(/^(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/),n){const e=n[1].split(",").map(e=>e.trim()).filter(Boolean);for(const t of e){const e=t.split(":").map(e=>e.trim()),i=(e[1]||e[0]).trim();i&&s.push({name:i,source:n[2],type:"import"})}return s}return n=t.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/),n?(s.push({name:n[1],source:n[2],type:"import"}),s):s}_findDeadClusters(e,t,s){const n=[],i=new Map;for(const t of e)t.isExport||t.isLifecycle||i.set(t.name,t);if(i.size<2)return n;const o=new Map;for(const[e,s]of i){const n=t.get(s.file);if(!n){o.set(e,new Set);continue}const r=n.split("\n").slice(s.line-1,s.line-1+s.lineCount),c=this._stripCommentsAndStrings(r.join("\n")),a=new Set;for(const[t]of i)t!==e&&new RegExp(`\\b${this._escapeRegex(t)}\\b`).test(c)&&a.add(t);o.set(e,a)}const r=new Map,c=e=>{r.has(e)||r.set(e,e);let t=e;for(;r.get(t)!==t;)t=r.get(t);let s=e;for(;r.get(s)!==t;){const e=r.get(s);r.set(s,t),s=e}return t},a=(e,t)=>{const s=c(e),n=c(t);s!==n&&r.set(s,n)};for(const[e,t]of o){c(e);for(const s of t)c(s),a(e,s)}const l=new Map;for(const e of i.keys()){const t=c(e);l.has(t)||l.set(t,[]),l.get(t).push(e)}for(const[,e]of l){if(e.length<2)continue;let t=!1;for(const s of e){for(const n of o.get(s)||[])if(e.includes(n)&&(o.get(n)||new Set).has(s)){t=!0;break}if(t)break}if(!t)continue;let r=!1;for(const t of e)if((s[t]||0)-e.reduce((e,s)=>s===t?e:e+((o.get(s)||new Set).has(t)?1:0),0)>0){r=!0;break}if(r)continue;const c=e.reduce((e,t)=>e+(i.get(t).lineCount||0),0);n.push({name:e.join(", "),file:i.get(e[0]).file,line:i.get(e[0]).line,lineCount:c,wastedLines:c,references:0,category:"dead_cluster",severity:"high",message:`Dead cluster: ${e.join(" ↔ ")} — mutually reference each other but nothing outside the cluster calls them`})}return n}_isLifecycle(e){return["constructor","render","componentDidMount","componentWillUnmount","componentDidUpdate","shouldComponentUpdate","getSnapshotBeforeUpdate","getDerivedStateFromProps","useEffect","useState","useMemo","toString","valueOf","toJSON","inspect","setup","teardown","beforeEach","afterEach","beforeAll","afterAll","init","destroy","configure","bootstrap","main","get","set","post","put","delete","patch","handle","onCreate","onStart","onResume","onPause","onStop","onDestroy","onCreateView","onViewCreated","onDestroyView","hashCode","equals","new","default","drop","fmt","from","into","clone","copy","initialize","to_s","to_str","inspect","freeze","dup","__construct","__destruct","__toString","__get","__set","__call","log_message","log_request","log_error"].includes(e)||/^__/.test(e)||/^on[A-Z]/.test(e)||/^visit_[A-Z]/.test(e)||"visit_comprehension"===e||/^do_[A-Z]/.test(e)||/^test_/.test(e)}_countFunctionLines(e,t,s){const n=(e[t]||"").trim(),i=new Set([".py",".pyw",".pyi",".pyx"]);if(s&&i.has(s)&&/^(?:async\s+)?def\s/.test(n)){const s=(e[t].match(/^[ \t]*/)||[""])[0].length;let n=1;for(let i=t+1;i<e.length&&i<t+200;i++){const t=e[i];if(""!==t.trim()){if((t.match(/^[ \t]*/)||[""])[0].length<=s)break;n++}else n++}return n}if(/^def\s/.test(n)){let s=1,n=1;for(let i=t+1;i<e.length&&i<t+200;i++){n++;const t=e[i].trim();if(/^(?:def|class|module|begin|case)\b/.test(t)&&s++,/^(?:if|unless|while|until|for)\b/.test(t)&&!/\bthen\b/.test(t)&&s++,/\bdo\s*(\|[^|]*\|)?\s*$/.test(t)&&s++,/^end\b/.test(t)&&s--,s<=0)break}return n}let o=0,r=!1,c=0;for(let s=t;s<e.length&&s<t+200;s++){const t=e[s];c++;for(const e of t)"{"===e&&(o++,r=!0),"}"===e&&o--;if(r&&o<=0)break}return c}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/index\.(js|ts)$/,/server\.(js|ts)$/,/app\.(js|ts)$/,/routes?\.(js|ts)$/,/middleware/i,/crucible\//i,/kenny-mode/i,/thuban-videos\//i,/regression\/fixtures\//i].some(t=>t.test(e))}_isFrameworkEntryFile(e){return/(^|[\/\\])(page|layout|loading|error|not-found|template|default|route)\.(js|jsx|ts|tsx)$/i.test(e)||/(^|[\/\\])middleware\.(js|ts)$/i.test(e)}_escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}_computeTemplateLiteralLines(e){const t=new Set,s=e.length;let n=0,i=0;for(;n<s;){const o=e[n],r=e[n+1];if("\n"!==o)if("/"===o&&"/"===r||"#"===o)for(;n<s&&"\n"!==e[n];)n++;else if("/"!==o||"*"!==r){if('"'===o||"'"===o){const t=o;for(n++;n<s&&e[n]!==t&&"\n"!==e[n];)"\\"===e[n]&&n++,n++;n=Math.min(n+1,s);continue}if("`"===o){const o=i,r=this._findTemplateEnd(e,n);for(let t=n;t<r&&t<s;t++)"\n"===e[t]&&i++;const c=i;if(c>o+1)for(let e=o+1;e<c;e++)t.add(e);n=r;continue}n++}else{for(n+=2;n<s&&("*"!==e[n]||"/"!==e[n+1]);)"\n"===e[n]&&i++,n++;n=Math.min(n+2,s)}else i++,n++}return t}_findTemplateEnd(e,t){let s=t+1;const n=e.length;for(;s<n;){const t=e[s];if("\\"!==t){if("`"===t)return s+1;if("$"===t&&"{"===e[s+1]){let t=1,i=s+2;for(;i<n&&t>0;){const s=e[i];if("{"===s)t++,i++;else if("}"===s)t--,i++;else if("`"===s)i=this._findTemplateEnd(e,i);else if('"'===s||"'"===s){const t=s;for(i++;i<n&&e[i]!==t;)"\\"===e[i]&&i++,i++;i++}else i++}s=i;continue}s++}else s+=2}return n}_stripTemplateLiteral(e,t){let s=t+1;const n=e.length,i=[];for(;s<n;){const t=e[s];if("\\"!==t){if("`"===t){s++;break}if("$"===t&&"{"===e[s+1]){let t=1,o=s+2;const r=o;for(;o<n&&t>0;){const s=e[o];if("{"===s)t++,o++;else if("}"===s)t--,o++;else if("`"===s)o=this._findTemplateEnd(e,o);else if('"'===s||"'"===s){const t=s;for(o++;o<n&&e[o]!==t;)"\\"===e[o]&&o++,o++;o++}else o++}i.push(e.slice(r,o-1)),s=o;continue}s++}else s+=2}return[i.length>0?`"" ${i.map(e=>this._stripCommentsAndStrings(e)).join(" ")} ""`:'""',s]}_canStartRegex(e){return!e||!/[A-Za-z0-9_$)\]]/.test(e)}_findRegexEnd(e,t){let s=t+1,n=!1;const i=e.length;for(;s<i;){const t=e[s];if("\n"===t)return-1;if("\\"!==t)if("["!==t)if("]"!==t){if("/"===t&&!n){for(s++;s<i&&/[a-zA-Z]/.test(e[s]);)s++;return s}s++}else n=!1,s++;else n=!0,s++;else s+=2}return-1}_stripCommentsAndStrings(e){let t="",s=0;const n=e.length;let i="";for(;s<n;){const o=e[s],r=e[s+1];if("/"===o&&"/"===r){let i=s+2;for(;i<n&&"\n"!==e[i];)i++;t+=" ",s=i;continue}if("#"===o){let i=s+1;for(;i<n&&"\n"!==e[i];)i++;t+=" ",s=i;continue}if("/"===o&&"*"===r){let i=s+2;for(;i<n&&("*"!==e[i]||"/"!==e[i+1]);)i++;i=Math.min(i+2,n),t+=" ",s=i;continue}if('"'===o){let o=s+1;for(;o<n&&'"'!==e[o]&&"\n"!==e[o];)"\\"===e[o]&&o++,o++;o=Math.min(o+1,n),t+='""',s=o,i='"';continue}if("'"===o){let o=s+1;for(;o<n&&"'"!==e[o]&&"\n"!==e[o];)"\\"===e[o]&&o++,o++;o=Math.min(o+1,n),t+="''",s=o,i="'";continue}if("`"===o){const[n,o]=this._stripTemplateLiteral(e,s);t+=n,s=o,i="`";continue}if("/"===o&&this._canStartRegex(i)){const n=this._findRegexEnd(e,s);if(-1!==n){t+=" ",s=n,i="/";continue}}t+=o,/\s/.test(o)||(i=o),s++}return t}}module.exports=GhostCodeDetector;
1
+ const fs=require("fs"),path=require("path");class GhostCodeDetector{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}scan(e){const t=[],s={},n=new Map,i=new Map,o=new Map;for(const r of e){let e;try{if(fs.statSync(r).size>524288)continue;e=fs.readFileSync(r,"utf-8").replace(/\r\n/g,"\n")}catch(e){continue}const c=path.relative(this.rootPath,r);if(this._isSkippable(c)){o.set(c,e);continue}const a=e.split("\n");n.set(c,e),i.set(c,a);const l=path.extname(r).toLowerCase(),f=this._computeTemplateLiteralLines(e);for(let e=0;e<a.length;e++){if(f.has(e))continue;const n=a[e],i=this._extractFunction(n,e,a,l);if(i){i.file=c,i.line=e+1,i.lineCount=this._countFunctionLines(a,e,l),i.declKind="function",i.isExport&&this._isFrameworkEntryFile(c)&&(i.isLifecycle=!0),t.push(i),s[i.name]||(s[i.name]=0);continue}const o=this._extractClass(n);if(o){o.file=c,o.line=e+1,o.lineCount=this._countFunctionLines(a,e,l),o.declKind="class",t.push(o),s[o.name]||(s[o.name]=0);continue}const r=this._extractConstDeclaration(n);if(r){r.file=c,r.line=e+1,r.lineCount=1,r.declKind="const",t.push(r),s[r.name]||(s[r.name]=0);continue}const u=this._extractImports(n);for(const n of u)n.file=c,n.line=e+1,n.lineCount=1,n.declKind="import",t.push(n),s[n.name]||(s[n.name]=0)}}const r=new Map;for(const e of t){let t=r.get(e.file);t||(t=new Map,r.set(e.file,t)),t.set(e.name,(t.get(e.name)||0)+1)}const c=new Map([...n,...o]);for(const[e,n]of c){const i=this._stripCommentsAndStrings(n),o=r.get(e);for(const e of t){const t=new RegExp(`\\b${this._escapeRegex(e.name)}\\b`,"g"),n=i.match(t);if(n){const t=o&&o.get(e.name)||0;s[e.name]+=Math.max(0,n.length-t)}}}const a=t.filter(e=>"function"===e.declKind).filter(e=>(s[e.name]||0)<=0&&!e.isLifecycle&&e.lineCount>=3).map(e=>({...e,references:s[e.name]||0,wastedLines:e.lineCount,category:"ghost",severity:e.isExport?"low":e.lineCount>30?"high":"medium",message:`${e.name}() — ${e.lineCount} lines, never called${e.isExport?" (exported — verify no external consumers)":""}`})),l=(e,n,i,o)=>t.filter(t=>t.declKind===e).filter(e=>(s[e.name]||0)<=0).map(e=>({...e,references:s[e.name]||0,wastedLines:e.lineCount,category:n,severity:i(e),message:o(e)})),f=l("class","ghost_class",e=>e.isExport?"low":"medium",e=>`class ${e.name} — ${e.lineCount} lines, never referenced${e.isExport?" (exported — verify no external consumers)":""}`),u=l("const","ghost_const",e=>e.isExport?"low":"medium",e=>`${e.kind} ${e.name} — declared but never used${e.isExport?" (exported — verify no external consumers)":""}`),p=l("import","ghost_import",()=>"low",e=>`import '${e.name}' from '${e.source}' — never used`),m=this._findDeadClusters(t.filter(e=>"function"===e.declKind),n,s),h=[...a,...f,...u,...p,...m].sort((e,t)=>t.wastedLines-e.wastedLines),d=h.reduce((e,t)=>e+t.wastedLines,0),_=e.reduce((e,t)=>{try{return e+fs.readFileSync(t,"utf-8").replace(/\r\n/g,"\n").split("\n").length}catch(t){return e}},0);return{ghosts:h,totalGhosts:h.length,totalWastedLines:d,wastedPercent:_>0?Math.min(100,Math.round(d/_*1e3)/10):0,totalLines:_,breakdown:{functions:a.length,classes:f.length,consts:u.length,imports:p.length,deadClusters:m.length}}}_extractFunction(e,t,s,n){const i=e.trim(),o=new Set([".py",".pyw",".pyi",".pyx"]),r=n&&o.has(n),c=i.replace(/^export\s+(?:default\s+)?/,"");let a=c.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);return a?{name:a[1],type:"function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:(a=c.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/),a?{name:a[1],type:"const-function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:(a=c.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/),a&&(c.includes("=>")||t+1<s.length&&s[t+1].includes("=>"))?{name:a[1],type:"arrow",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(a[1])}:r&&(a=i.match(/^(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(\[]/),a)?{name:a[1],type:"python-def",isExport:!/^_/.test(a[1]),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),a?{name:a[1],type:"kotlin-fun",isExport:!i.startsWith("private"),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),a?{name:a[1],type:"go-func",isExport:/^[A-Z]/.test(a[1]),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),a?{name:a[1],type:"rust-fn",isExport:/^pub/.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),a?{name:a[1],type:"php-function",isExport:!/private/.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),a?{name:a[1],type:"ruby-def",isExport:/^def\s+self\./.test(i),isLifecycle:this._isLifecycle(a[1])}:(a=i.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/),a&&!["if","for","while","switch","catch","constructor","render","toString"].includes(a[1])?{name:a[1],type:"method",isExport:!1,isLifecycle:this._isLifecycle(a[1])}:null))))))))}_isExported(e,t,s){return!!/^export\s/.test(e.trim())||!!/module\.exports/.test(e)}_extractClass(e){const t=e.trim(),s=t.match(/^(?:export\s+(?:default\s+)?)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return s?{name:s[1],type:"class",isExport:/^export\s/.test(t),isLifecycle:!1}:null}_extractConstDeclaration(e){const t=e.trim().match(/^(export\s+)?(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)$/);if(!t)return null;const[,s,n,i,o]=t;return/^(?:async\s+)?function\b/.test(o)||/=>/.test(o)||""===o.trim()||/^\(/.test(o.trim())&&/=>/.test(o)||/require\s*\(/.test(o)?null:{name:i,kind:n,type:"const-declaration",isExport:!!s,isLifecycle:!1}}_extractImports(e){const t=e.trim(),s=[];let n=t.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);if(n){const e=n[1].split(",").map(e=>e.trim()).filter(Boolean);for(const t of e){const e=t.split(/\s+as\s+/),i=(e[1]||e[0]).trim();i&&s.push({name:i,source:n[2],type:"import"})}return s}if(n=t.match(/^import\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:,|from)/),n&&!/^import\s*\{/.test(t)){const e=t.match(/from\s*['"]([^'"]+)['"]/);return s.push({name:n[1],source:e?e[1]:"",type:"import"}),s}if(n=t.match(/^import\s*\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*from\s*['"]([^'"]+)['"]/),n)return s.push({name:n[1],source:n[2],type:"import"}),s;if(n=t.match(/^(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/),n){const e=n[1].split(",").map(e=>e.trim()).filter(Boolean);for(const t of e){const e=t.split(":").map(e=>e.trim()),i=(e[1]||e[0]).trim();i&&s.push({name:i,source:n[2],type:"import"})}return s}return n=t.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/),n?(s.push({name:n[1],source:n[2],type:"import"}),s):s}_findDeadClusters(e,t,s){const n=[],i=new Map;for(const t of e)t.isExport||t.isLifecycle||i.set(t.name,t);if(i.size<2)return n;const o=new Map;for(const[e,s]of i){const n=t.get(s.file);if(!n){o.set(e,new Set);continue}const r=n.split("\n").slice(s.line-1,s.line-1+s.lineCount),c=this._stripCommentsAndStrings(r.join("\n")),a=new Set;for(const[t]of i)t!==e&&new RegExp(`\\b${this._escapeRegex(t)}\\b`).test(c)&&a.add(t);o.set(e,a)}let r=0;const c=new Map,a=new Map,l=new Set,f=[],u=[],p=e=>{c.set(e,r),a.set(e,r),r++,f.push(e),l.add(e);for(const t of o.get(e)||[])i.has(t)&&(c.has(t)?l.has(t)&&a.set(e,Math.min(a.get(e),c.get(t))):(p(t),a.set(e,Math.min(a.get(e),a.get(t)))));if(a.get(e)===c.get(e)){const t=[];let s;do{s=f.pop(),l.delete(s),t.push(s)}while(s!==e);u.push(t)}};for(const e of i.keys())c.has(e)||p(e);for(const e of u){if(e.length<2){const[t]=e;if(!(o.get(t)||new Set).has(t))continue}let t=!1;for(const n of e)if((s[n]||0)-e.reduce((e,t)=>t===n?e:e+((o.get(t)||new Set).has(n)?1:0),0)>0){t=!0;break}if(t)continue;const r=e.reduce((e,t)=>e+(i.get(t).lineCount||0),0);n.push({name:e.join(", "),file:i.get(e[0]).file,line:i.get(e[0]).line,lineCount:r,wastedLines:r,references:0,category:"dead_cluster",severity:"high",message:`Dead cluster: ${e.join(" ↔ ")} — mutually reference each other but nothing outside the cluster calls them`})}return n}_isLifecycle(e){return["constructor","render","componentDidMount","componentWillUnmount","componentDidUpdate","shouldComponentUpdate","getSnapshotBeforeUpdate","getDerivedStateFromProps","useEffect","useState","useMemo","toString","valueOf","toJSON","inspect","setup","teardown","beforeEach","afterEach","beforeAll","afterAll","init","destroy","configure","bootstrap","main","get","set","post","put","delete","patch","handle","onCreate","onStart","onResume","onPause","onStop","onDestroy","onCreateView","onViewCreated","onDestroyView","hashCode","equals","new","default","drop","fmt","from","into","clone","copy","initialize","to_s","to_str","inspect","freeze","dup","__construct","__destruct","__toString","__get","__set","__call","log_message","log_request","log_error"].includes(e)||/^__/.test(e)||/^on[A-Z]/.test(e)||/^visit_[A-Z]/.test(e)||"visit_comprehension"===e||/^do_[A-Z]/.test(e)||/^test_/.test(e)}_countFunctionLines(e,t,s){const n=(e[t]||"").trim(),i=new Set([".py",".pyw",".pyi",".pyx"]);if(s&&i.has(s)&&/^(?:async\s+)?def\s/.test(n)){const s=(e[t].match(/^[ \t]*/)||[""])[0].length;let n=1;for(let i=t+1;i<e.length&&i<t+200;i++){const t=e[i];if(""!==t.trim()){if((t.match(/^[ \t]*/)||[""])[0].length<=s)break;n++}else n++}return n}if(/^def\s/.test(n)){let s=1,n=1;for(let i=t+1;i<e.length&&i<t+200;i++){n++;const t=e[i].trim();if(/^(?:def|class|module|begin|case)\b/.test(t)&&s++,/^(?:if|unless|while|until|for)\b/.test(t)&&!/\bthen\b/.test(t)&&s++,/\bdo\s*(\|[^|]*\|)?\s*$/.test(t)&&s++,/^end\b/.test(t)&&s--,s<=0)break}return n}let o=0,r=!1,c=0;for(let s=t;s<e.length&&s<t+200;s++){const t=e[s];c++;for(const e of t)"{"===e&&(o++,r=!0),"}"===e&&o--;if(r&&o<=0)break}return c}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/index\.(js|ts)$/,/server\.(js|ts)$/,/app\.(js|ts)$/,/routes?\.(js|ts)$/,/middleware/i,/crucible\//i,/kenny-mode/i,/thuban-videos\//i,/regression\/fixtures\//i].some(t=>t.test(e))}_isFrameworkEntryFile(e){return/(^|[\/\\])(page|layout|loading|error|not-found|template|default|route)\.(js|jsx|ts|tsx)$/i.test(e)||/(^|[\/\\])middleware\.(js|ts)$/i.test(e)}_escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}_computeTemplateLiteralLines(e){const t=new Set,s=e.length;let n=0,i=0;for(;n<s;){const o=e[n],r=e[n+1];if("\n"!==o)if("/"===o&&"/"===r||"#"===o)for(;n<s&&"\n"!==e[n];)n++;else if("/"!==o||"*"!==r){if('"'===o||"'"===o){const t=o;for(n++;n<s&&e[n]!==t&&"\n"!==e[n];)"\\"===e[n]&&n++,n++;n=Math.min(n+1,s);continue}if("`"===o){const o=i,r=this._findTemplateEnd(e,n);for(let t=n;t<r&&t<s;t++)"\n"===e[t]&&i++;const c=i;if(c>o+1)for(let e=o+1;e<c;e++)t.add(e);n=r;continue}n++}else{for(n+=2;n<s&&("*"!==e[n]||"/"!==e[n+1]);)"\n"===e[n]&&i++,n++;n=Math.min(n+2,s)}else i++,n++}return t}_findTemplateEnd(e,t){let s=t+1;const n=e.length;for(;s<n;){const t=e[s];if("\\"!==t){if("`"===t)return s+1;if("$"===t&&"{"===e[s+1]){let t=1,i=s+2;for(;i<n&&t>0;){const s=e[i];if("{"===s)t++,i++;else if("}"===s)t--,i++;else if("`"===s)i=this._findTemplateEnd(e,i);else if('"'===s||"'"===s){const t=s;for(i++;i<n&&e[i]!==t;)"\\"===e[i]&&i++,i++;i++}else i++}s=i;continue}s++}else s+=2}return n}_stripTemplateLiteral(e,t){let s=t+1;const n=e.length,i=[];for(;s<n;){const t=e[s];if("\\"!==t){if("`"===t){s++;break}if("$"===t&&"{"===e[s+1]){let t=1,o=s+2;const r=o;for(;o<n&&t>0;){const s=e[o];if("{"===s)t++,o++;else if("}"===s)t--,o++;else if("`"===s)o=this._findTemplateEnd(e,o);else if('"'===s||"'"===s){const t=s;for(o++;o<n&&e[o]!==t;)"\\"===e[o]&&o++,o++;o++}else o++}i.push(e.slice(r,o-1)),s=o;continue}s++}else s+=2}return[i.length>0?`"" ${i.map(e=>this._stripCommentsAndStrings(e)).join(" ")} ""`:'""',s]}_canStartRegex(e){return!e||!/[A-Za-z0-9_$)\]]/.test(e)}_findRegexEnd(e,t){let s=t+1,n=!1;const i=e.length;for(;s<i;){const t=e[s];if("\n"===t)return-1;if("\\"!==t)if("["!==t)if("]"!==t){if("/"===t&&!n){for(s++;s<i&&/[a-zA-Z]/.test(e[s]);)s++;return s}s++}else n=!1,s++;else n=!0,s++;else s+=2}return-1}_stripCommentsAndStrings(e){let t="",s=0;const n=e.length;let i="";for(;s<n;){const o=e[s],r=e[s+1];if("/"===o&&"/"===r){let i=s+2;for(;i<n&&"\n"!==e[i];)i++;t+=" ",s=i;continue}if("#"===o){let i=s+1;for(;i<n&&"\n"!==e[i];)i++;t+=" ",s=i;continue}if("/"===o&&"*"===r){let i=s+2;for(;i<n&&("*"!==e[i]||"/"!==e[i+1]);)i++;i=Math.min(i+2,n),t+=" ",s=i;continue}if('"'===o){let o=s+1;for(;o<n&&'"'!==e[o]&&"\n"!==e[o];)"\\"===e[o]&&o++,o++;o=Math.min(o+1,n),t+='""',s=o,i='"';continue}if("'"===o){let o=s+1;for(;o<n&&"'"!==e[o]&&"\n"!==e[o];)"\\"===e[o]&&o++,o++;o=Math.min(o+1,n),t+="''",s=o,i="'";continue}if("`"===o){const[n,o]=this._stripTemplateLiteral(e,s);t+=n,s=o,i="`";continue}if("/"===o&&this._canStartRegex(i)){const n=this._findRegexEnd(e,s);if(-1!==n){t+=" ",s=n,i="/";continue}}t+=o,/\s/.test(o)||(i=o),s++}return t}}module.exports=GhostCodeDetector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
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"
@@ -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
- )
@@ -1,37 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: critical
4
- # language: python
5
- # expected: command injection via os.system with user-controlled input
6
-
7
- import os
8
- from flask import Flask, request, jsonify
9
-
10
- app = Flask(__name__)
11
-
12
-
13
- @app.route("/tools/ping")
14
- def ping():
15
- host = request.args.get("host", "")
16
- # Attacker: host = "8.8.8.8; cat /etc/passwd"
17
- exit_code = os.system(f"ping -c 4 {host}")
18
- return jsonify({"exit_code": exit_code})
19
-
20
-
21
- @app.route("/tools/convert", methods=["POST"])
22
- def convert_file():
23
- data = request.get_json()
24
- input_file = data.get("input", "")
25
- output_format = data.get("format", "pdf")
26
- # os.system executes in shell — injection via semicolons, backticks, etc.
27
- os.system(f"convert {input_file} output.{output_format}")
28
- return jsonify({"status": "done"})
29
-
30
-
31
- @app.route("/tools/compress", methods=["POST"])
32
- def compress():
33
- data = request.get_json()
34
- folder = data.get("folder", "")
35
- name = data.get("archive_name", "archive")
36
- os.system(f"tar -czf /tmp/{name}.tar.gz {folder}")
37
- return jsonify({"archive": f"/tmp/{name}.tar.gz"})
@@ -1,38 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: critical
4
- # language: python
5
- # expected: command injection via subprocess.call with shell=True and user input
6
-
7
- import subprocess
8
- from flask import Flask, request, jsonify
9
-
10
- app = Flask(__name__)
11
-
12
-
13
- @app.route("/git/clone", methods=["POST"])
14
- def git_clone():
15
- data = request.get_json()
16
- repo_url = data.get("url", "")
17
- target_dir = data.get("dir", "/tmp/repo")
18
- # shell=True means the full string is passed to /bin/sh
19
- ret = subprocess.call(f"git clone {repo_url} {target_dir}", shell=True)
20
- return jsonify({"return_code": ret})
21
-
22
-
23
- @app.route("/nmap", methods=["POST"])
24
- def nmap_scan():
25
- target = request.get_json().get("target", "")
26
- ports = request.get_json().get("ports", "1-1000")
27
- # target and ports from user — shell injection via semicolons
28
- result = subprocess.check_output(
29
- f"nmap -p {ports} {target}", shell=True, text=True
30
- )
31
- return jsonify({"output": result})
32
-
33
-
34
- @app.route("/whois")
35
- def whois():
36
- domain = request.args.get("domain", "")
37
- output = subprocess.check_output(f"whois {domain}", shell=True, text=True)
38
- return jsonify({"result": output})
@@ -1,40 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: critical
4
- # language: python
5
- # expected: command injection via subprocess.Popen with shell=True and f-string
6
-
7
- import subprocess
8
- from fastapi import FastAPI
9
- from pydantic import BaseModel
10
-
11
- app = FastAPI()
12
-
13
-
14
- class ImageRequest(BaseModel):
15
- image_path: str # user-controlled
16
- width: int
17
- height: int
18
- quality: int = 85
19
-
20
-
21
- class ReportRequest(BaseModel):
22
- format: str # user-controlled
23
- date_range: str # user-controlled
24
-
25
-
26
- @app.post("/images/thumbnail")
27
- def generate_thumbnail(req: ImageRequest):
28
- # image_path is user-controlled — injection via shell metacharacters
29
- cmd = f"ffmpeg -i {req.image_path} -vf scale={req.width}:{req.height} -q:v {req.quality} out.jpg"
30
- proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
31
- out, err = proc.communicate(timeout=30)
32
- return {"output": out.decode(), "errors": err.decode()}
33
-
34
-
35
- @app.post("/reports/generate")
36
- def generate_report(req: ReportRequest):
37
- cmd = f"python3 scripts/report.py --format {req.format} --range {req.date_range}"
38
- proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
39
- out, _ = proc.communicate()
40
- return {"path": out.decode().strip()}
@@ -1,35 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: high
4
- # language: python
5
- # expected: command injection via os.popen with user-controlled filename
6
-
7
- import os
8
- from flask import Flask, request, send_file
9
- import io
10
-
11
- app = Flask(__name__)
12
-
13
-
14
- @app.route("/export/csv")
15
- def export_csv():
16
- table = request.args.get("table", "users")
17
- date = request.args.get("date", "2024-01-01")
18
- # table and date injected via shell — attacker: table = "users; id > /tmp/pwned"
19
- output = os.popen(f"psql -c 'SELECT * FROM {table} WHERE date > \"{date}\"' -A -F, mydb").read()
20
- return output, 200, {"Content-Type": "text/csv"}
21
-
22
-
23
- @app.route("/preview/pdf")
24
- def preview_pdf():
25
- filename = request.args.get("file", "")
26
- # filename from user — injection via backticks or semicolons
27
- output = os.popen(f"pdftotext uploads/{filename} -").read()
28
- return {"text": output}
29
-
30
-
31
- @app.route("/image/metadata")
32
- def image_metadata():
33
- path = request.args.get("path", "")
34
- result = os.popen(f"exiftool {path}").read()
35
- return {"metadata": result}
@@ -1,35 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: high
4
- # language: python
5
- # expected: command injection via paramiko exec_command with user-controlled command string
6
-
7
- import paramiko
8
- from flask import Flask, request, jsonify
9
-
10
- app = Flask(__name__)
11
-
12
-
13
- def get_ssh_client(host: str) -> paramiko.SSHClient:
14
- client = paramiko.SSHClient()
15
- client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
16
- client.connect(host, username="deploy", key_filename="/home/deploy/.ssh/id_rsa")
17
- return client
18
-
19
-
20
- @app.route("/ssh/run", methods=["POST"])
21
- def run_remote_command():
22
- data = request.get_json()
23
- host = data.get("host", "")
24
- # command from user — attacker appends malicious commands
25
- command = data.get("command", "")
26
- allowed_hosts = ["web-01.internal", "web-02.internal"]
27
- if host not in allowed_hosts:
28
- return jsonify({"error": "Host not allowed"}), 403
29
-
30
- client = get_ssh_client(host)
31
- # command not validated — attacker sends "ls; cat /etc/shadow"
32
- stdin, stdout, stderr = client.exec_command(command)
33
- output = stdout.read().decode()
34
- client.close()
35
- return jsonify({"output": output})
@@ -1,42 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: command_injection
3
- # severity: critical
4
- # language: python
5
- # expected: command injection via subprocess.run with shell=True and user-controlled git operation
6
-
7
- import subprocess
8
- from dataclasses import dataclass
9
-
10
-
11
- @dataclass
12
- class GitConfig:
13
- repo_path: str
14
- branch: str # user-controlled
15
- remote: str = "origin"
16
-
17
-
18
- class GitService:
19
- def clone(self, repo_url: str, target: str) -> str:
20
- # repo_url from user — injection possible
21
- result = subprocess.run(
22
- f"git clone {repo_url} {target}", shell=True, capture_output=True, text=True
23
- )
24
- return result.stdout
25
-
26
- def checkout(self, cfg: GitConfig) -> str:
27
- # cfg.branch from user — "main; cat /etc/passwd > /tmp/leak"
28
- result = subprocess.run(
29
- f"git -C {cfg.repo_path} checkout {cfg.branch}",
30
- shell=True, capture_output=True, text=True
31
- )
32
- return result.stdout
33
-
34
- def tag(self, repo: str, tag_name: str, message: str) -> str:
35
- result = subprocess.run(
36
- f'git -C {repo} tag -a {tag_name} -m "{message}"',
37
- shell=True, capture_output=True, text=True
38
- )
39
- return result.stdout
40
-
41
- def archive(self, repo: str, branch: str, output_file: str) -> None:
42
- subprocess.run(f"git -C {repo} archive {branch} > {output_file}", shell=True)
@@ -1,37 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: path_traversal
3
- # severity: high
4
- # language: python
5
- # expected: path traversal in Django view via user-controlled filename parameter
6
-
7
- import os
8
- from django.http import FileResponse, Http404, HttpResponse
9
- from django.conf import settings
10
-
11
-
12
- def serve_user_file(request, user_id: str):
13
- filename = request.GET.get("file", "")
14
- # No normalization — filename can be "../../../etc/passwd"
15
- file_path = os.path.join(settings.MEDIA_ROOT, user_id, filename)
16
- try:
17
- return FileResponse(open(file_path, "rb"))
18
- except FileNotFoundError:
19
- raise Http404("File not found")
20
-
21
-
22
- def export_report(request):
23
- report_name = request.GET.get("report", "monthly")
24
- locale = request.GET.get("locale", "en")
25
- # Both report_name and locale inserted into path — traversal possible
26
- path = os.path.join(settings.REPORTS_DIR, locale, f"{report_name}.pdf")
27
- if not os.path.exists(path):
28
- raise Http404
29
- with open(path, "rb") as f:
30
- return HttpResponse(f.read(), content_type="application/pdf")
31
-
32
-
33
- def read_template(request):
34
- template = request.GET.get("template", "welcome")
35
- path = f"{settings.TEMPLATES_DIR}/{template}.html"
36
- with open(path) as f:
37
- return HttpResponse(f.read())
@@ -1,34 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: path_traversal
3
- # severity: high
4
- # language: python
5
- # expected: path traversal via zipfile extraction without entry path validation (zip-slip)
6
-
7
- import os
8
- import zipfile
9
-
10
-
11
- EXTRACT_BASE = "/var/app/uploads/extracted"
12
-
13
-
14
- def extract_archive(zip_bytes: bytes, user_id: str) -> list:
15
- import io
16
- extracted = []
17
- with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
18
- for entry in zf.infolist():
19
- # entry.filename can contain "../" — writes outside EXTRACT_BASE
20
- output_path = os.path.join(EXTRACT_BASE, user_id, entry.filename)
21
- if entry.is_dir():
22
- os.makedirs(output_path, exist_ok=True)
23
- else:
24
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
25
- with zf.open(entry) as src, open(output_path, "wb") as dst:
26
- dst.write(src.read())
27
- extracted.append(output_path)
28
- return extracted
29
-
30
-
31
- def get_extracted_file(user_id: str, relative_path: str) -> bytes:
32
- full_path = os.path.join(EXTRACT_BASE, user_id, relative_path)
33
- with open(full_path, "rb") as f:
34
- return f.read()
@@ -1,33 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: insecure_crypto
3
- # severity: high
4
- # language: python
5
- # expected: DES cipher usage and AES-ECB mode with static key
6
-
7
- from Crypto.Cipher import DES, AES
8
- import base64
9
-
10
- DES_KEY = b"weakkey!" # 8 bytes — DES is broken
11
- AES_KEY = b"0123456789abcdef" # 16 bytes
12
-
13
-
14
- def des_encrypt(data: str) -> str:
15
- # DES has only 56-bit effective key length — trivially brute-forced
16
- padded = data + " " * (8 - len(data) % 8)
17
- cipher = DES.new(DES_KEY, DES.MODE_ECB)
18
- return base64.b64encode(cipher.encrypt(padded.encode())).decode()
19
-
20
-
21
- def aes_ecb_encrypt(data: str) -> str:
22
- # ECB mode: identical plaintext blocks produce identical ciphertext blocks
23
- pad_len = 16 - len(data) % 16
24
- padded = (data + chr(pad_len) * pad_len).encode()
25
- cipher = AES.new(AES_KEY, AES.MODE_ECB)
26
- return base64.b64encode(cipher.encrypt(padded)).decode()
27
-
28
-
29
- def aes_ecb_decrypt(ciphertext: str) -> str:
30
- cipher = AES.new(AES_KEY, AES.MODE_ECB)
31
- decrypted = cipher.decrypt(base64.b64decode(ciphertext))
32
- pad_len = decrypted[-1]
33
- return decrypted[:-pad_len].decode()
@@ -1,41 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: insecure_crypto
3
- # severity: medium
4
- # language: python
5
- # expected: timing attack via string comparison of HMAC signatures, low-iteration PBKDF2
6
-
7
- import hmac
8
- import hashlib
9
-
10
-
11
- WEBHOOK_SECRET = b"static-webhook-secret-2024"
12
-
13
-
14
- def compute_signature(payload: bytes) -> str:
15
- return hmac.new(WEBHOOK_SECRET, payload, hashlib.sha256).hexdigest()
16
-
17
-
18
- def verify_signature(payload: bytes, received_sig: str) -> bool:
19
- expected = compute_signature(payload)
20
- # String comparison leaks timing info — use hmac.compare_digest instead
21
- return expected == received_sig
22
-
23
-
24
- def verify_api_key(provided: str, stored: str) -> bool:
25
- # Direct equality — timing side-channel attack possible
26
- return provided == stored
27
-
28
-
29
- def derive_key(password: str) -> bytes:
30
- # Only 1000 iterations — far below NIST recommendation of 600,000+
31
- return hashlib.pbkdf2_hmac("sha1", password.encode(), b"static-salt", 1000)
32
-
33
-
34
- def check_token(token_a: str, token_b: str) -> bool:
35
- # Char-by-char comparison leaks prefix matches
36
- if len(token_a) != len(token_b):
37
- return False
38
- for a, b in zip(token_a, token_b):
39
- if a != b:
40
- return False
41
- return True
@@ -1,33 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: insecure_crypto
3
- # severity: high
4
- # language: python
5
- # expected: MD5 used for file integrity and RSA with insufficient key size
6
-
7
- import hashlib
8
- from Crypto.PublicKey import RSA
9
- from Crypto.Signature import pkcs1_15
10
- from Crypto.Hash import MD5
11
-
12
-
13
- def checksum_file(filepath: str) -> str:
14
- # MD5 is collision-prone — not suitable for integrity verification
15
- with open(filepath, "rb") as f:
16
- return hashlib.md5(f.read()).hexdigest()
17
-
18
-
19
- def verify_file_integrity(filepath: str, expected_md5: str) -> bool:
20
- return checksum_file(filepath) == expected_md5
21
-
22
-
23
- def generate_rsa_keypair():
24
- # 512-bit RSA key — trivially factorable in seconds
25
- key = RSA.generate(512)
26
- return key.export_key().decode(), key.publickey().export_key().decode()
27
-
28
-
29
- def sign_with_md5(private_key_pem: str, message: bytes) -> bytes:
30
- key = RSA.import_key(private_key_pem)
31
- # MD5 with RSA — known-weak combination
32
- h = MD5.new(message)
33
- return pkcs1_15.new(key).sign(h)
@@ -1,39 +0,0 @@
1
- # [CRUCIBLE-SEED]
2
- # category: eval_abuse
3
- # severity: critical
4
- # language: python
5
- # expected: eval() called with user-controlled expression string
6
-
7
- from flask import Flask, request, jsonify
8
-
9
- app = Flask(__name__)
10
-
11
-
12
- @app.route("/calculate", methods=["POST"])
13
- def calculate():
14
- data = request.get_json()
15
- expression = data.get("expression", "")
16
- try:
17
- # Attacker sends: "__import__('os').system('id')"
18
- result = eval(expression)
19
- return jsonify({"result": result})
20
- except Exception as e:
21
- return jsonify({"error": str(e)}), 400
22
-
23
-
24
- @app.route("/filter", methods=["POST"])
25
- def filter_data():
26
- data = request.get_json()
27
- records = data.get("records", [])
28
- condition = data.get("condition", "True")
29
- # Attacker sends: "True or __import__('subprocess').check_output(['id'])"
30
- filtered = [r for r in records if eval(condition, {"r": r})]
31
- return jsonify({"results": filtered})
32
-
33
-
34
- @app.route("/transform", methods=["POST"])
35
- def transform():
36
- expr = request.get_json().get("expr", "x")
37
- items = request.get_json().get("items", [1, 2, 3])
38
- result = list(map(lambda x: eval(expr), items))
39
- return jsonify({"result": result})