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.
- package/dist/cli.js +1 -1
- package/dist/package.json +1 -1
- package/dist/packages/crucible/rules/code-scanner-core.json +11 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -1
- package/package.json +1 -1
- package/dist/packages/crucible/seeds/python/seed-021 2.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-022 2.py +0 -34
- package/dist/packages/crucible/seeds/python/seed-023 2.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-024 2.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-025 2.py +0 -40
- package/dist/packages/crucible/seeds/python/seed-026 2.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-027 2.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-028 2.py +0 -42
- package/dist/packages/crucible/seeds/python/seed-030 2.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-031 2.py +0 -34
- package/dist/packages/crucible/seeds/python/seed-036 2.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-037 2.py +0 -41
- package/dist/packages/crucible/seeds/python/seed-038 2.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-039 2.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-040 2.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-041 2.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-042 2.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-043 2.py +0 -32
- package/dist/packages/crucible/seeds/python/seed-044 2.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-045 2.py +0 -36
- package/dist/packages/crucible/seeds/python/seed-046 2.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-047 2.py +0 -44
- package/dist/packages/crucible/seeds/python/seed-048 2.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-049 2.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-050 2.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-051 2.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-052 2.py +0 -41
- package/dist/packages/crucible/seeds/ruby/seed-001 2.rb +0 -15
- package/dist/packages/crucible/seeds/ruby/seed-002 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-003 2.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-004 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-005 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-006 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-007 2.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-008 2.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-009 2.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-010 2.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-011 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-012 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-013 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-014 2.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-015 2.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-016 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-017 2.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-018 2.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-019 2.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-020 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-021 2.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-022 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-023 2.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-024 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-025 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-026 2.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-027 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-028 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-029 2.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-030 2.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-031 2.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-032 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-033 2.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-034 2.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-035 2.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-036 2.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-037 2.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-038 2.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-039 2.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-040 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-041 2.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-042 2.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-043 2.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-044 2.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-045 2.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-046 2.rb +0 -27
- package/dist/packages/crucible/seeds/ruby/seed-047 2.rb +0 -26
- package/dist/packages/crucible/seeds/ruby/seed-048 2.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-049 2.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-050 2.rb +0 -27
- package/dist/packages/crucible/seeds/rust/seed-001 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-002 2.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-003 2.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-004 2.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-005 2.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-006 2.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-007 2.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-008 2.rs +0 -19
- package/dist/packages/crucible/seeds/rust/seed-009 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-010 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-011 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-012 2.rs +0 -31
- package/dist/packages/crucible/seeds/rust/seed-013 2.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-014 2.rs +0 -30
- package/dist/packages/crucible/seeds/rust/seed-015 2.rs +0 -33
- package/dist/packages/crucible/seeds/rust/seed-016 2.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-017 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-018 2.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-019 2.rs +0 -36
- package/dist/packages/crucible/seeds/rust/seed-020 2.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-021 2.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-022 2.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-023 2.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-024 2.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-025 2.rs +0 -29
- package/dist/packages/crucible/seeds/rust/seed-026 2.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-027 2.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-028 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-029 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-030 2.rs +0 -30
- package/dist/packages/crucible/seeds/rust/seed-031 2.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-032 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-033 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-034 2.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-035 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-036 2.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-037 2.rs +0 -31
- package/dist/packages/crucible/seeds/rust/seed-038 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-039 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-040 2.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-041 2.rs +0 -32
- package/dist/packages/crucible/seeds/rust/seed-042 2.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-043 2.rs +0 -29
- package/dist/packages/crucible/seeds/rust/seed-044 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-045 2.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-046 2.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-047 2.rs +0 -34
- package/dist/packages/crucible/seeds/rust/seed-048 2.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-049 2.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-050 2.rs +0 -23
- package/dist/packages/crucible/seeds/ts/seed-001 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-002 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-003 2.ts +0 -28
- package/dist/packages/crucible/seeds/ts/seed-004 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-005 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-006 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-007 2.ts +0 -28
- package/dist/packages/crucible/seeds/ts/seed-008 2.ts +0 -40
- package/dist/packages/crucible/seeds/ts/seed-009 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-010 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-011 2.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-012 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-013 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-014 2.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-015 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-016 2.ts +0 -37
- package/dist/packages/crucible/seeds/ts/seed-017 2.ts +0 -44
- package/dist/packages/crucible/seeds/ts/seed-018 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-019 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-020 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-021 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-022 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-023 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-024 2.ts +0 -35
- package/dist/packages/crucible/seeds/ts/seed-025 2.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-026 2.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-027 2.ts +0 -30
- package/dist/packages/crucible/seeds/ts/seed-028 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-029 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-030 2.ts +0 -37
- package/dist/packages/crucible/seeds/ts/seed-031 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-032 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-033 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-034 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-035 2.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-036 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-037 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-038 2.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-039 2.ts +0 -35
- package/dist/packages/crucible/seeds/ts/seed-040 2.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-041 2.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-042 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-043 2.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-044 2.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-045 2.ts +0 -39
- package/dist/packages/crucible/seeds/ts/seed-046 2.ts +0 -41
- package/dist/packages/crucible/seeds/ts/seed-047 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-048 2.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-049 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-050 2.ts +0 -41
- package/dist/packages/crucible/seeds/ts/seed-051 2.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-052 2.ts +0 -36
package/dist/package.json
CHANGED
|
@@ -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.
|
|
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})
|