thuban 0.4.5 → 0.4.6
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/README.md +437 -420
- package/dist/README.md +437 -420
- package/dist/cli.js +1 -2
- package/dist/package.json +6 -4
- package/dist/packages/crucible/mutation-engine.js +1 -1
- package/dist/packages/crucible/pattern-learner.js +1 -1
- package/dist/packages/crucible/rule-loader.js +1 -1
- package/dist/packages/crucible/rules/code-scanner-core.json +72 -24
- package/dist/packages/crucible/seeding-engine.js +1 -1
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -1
- package/dist/packages/scanner/code-scanner.js +1 -1
- package/dist/packages/scanner/codebase-passport.js +1 -1
- package/dist/packages/scanner/copy-paste-detector.js +1 -1
- package/dist/packages/scanner/dependency-graph.js +1 -1
- package/dist/packages/scanner/drift-detector.js +1 -1
- package/dist/packages/scanner/export-verifier.js +1 -1
- package/dist/packages/scanner/file-collector.js +1 -1
- package/dist/packages/scanner/ghost-code-detector.js +1 -1
- package/dist/packages/scanner/hallucination-detector.js +1 -1
- package/dist/packages/scanner/license-manager.js +1 -1
- package/dist/packages/scanner/pre-commit-gate.js +1 -1
- package/dist/packages/scanner/read-file.js +1 -0
- package/dist/packages/scanner/taint-tracker.js +1 -1
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),MUTATIONS=[{name:"operator_flip_eq",description:"Replace === with == (loose equality — security bypass risk)",apply:e=>e.replace(/===/g,"==").replace(/!==/g,"!=")},{name:"operator_flip_and",description:"Replace && with & (bitwise — can bypass auth checks)",apply:e=>e.replace(/if\s*\(([^)]+)\)/g,(e,t)=>"if ("+t.replace(/&&/g,"&")+")")},{name:"operator_flip_or",description:"Replace || with | (bitwise — default value bypass)",apply:e=>e.replace(/if\s*\(([^)]+)\)/g,(e,t)=>"if ("+t.replace(/\|\|/g,"|")+")")},{name:"string_concat_to_template",description:'Convert "a" + b to `a${b}` (template literal — SQL injection risk)',apply:e=>e.replace(/"([^"]{1,40}?)"\s*\+\s*(\w+)/g,(e,t,s)=>"`"+t+"${"+s+"}`")},{name:"string_split_concat",description:"Split a hardcoded string across two literals (obfuscation)",apply:e=>e.replace(/'([a-zA-Z0-9_\-]{8,24})'/g,(e,t)=>{const s=Math.floor(t.length/2);return`'${t.slice(0,s)}' + '${t.slice(s)}'`})},{name:"base64_encode_string",description:'Wrap a string value in Buffer.from().toString("base64") (obfuscation)',apply:(e,t)=>["js","ts","javascript","typescript"].includes(t)?e.replace(/'(password|secret|key|token|api_?key)':\s*'([^']+)'/gi,(e,t,s)=>`'${t}': Buffer.from('${Buffer.from(s).toString("base64")}', 'base64').toString('utf8')`):e},{name:"require_to_dynamic_import",description:"Convert require() to eval-based dynamic require (evades static analysis)",apply(e,t){if(!["js","javascript"].includes(t))return e;let s=0;return e.replace(/require\('([^']+)'\)/g,(e,t)=>s++>0?e:`eval("require")('${t}')`)}},{name:"import_alias_obfuscation",description:"Add an alias variable to obscure dangerous function names",apply(e,t){if(!["js","ts","javascript","typescript"].includes(t))return e;if(/\bexecSync\b/.test(e)){const t="_"+crypto.randomBytes(3).toString("hex");e=(e="const "+t+' = require("child_process").execSync;\n'+e).replace(/\bexecSync\b/g,t)}return e}},{name:"if_negation_flip",description:"Flip if/else branches (may invert security checks)",apply:e=>e.replace(/if\s*\(!\s*(\w+)\)\s*\{([^}]*)\}\s*else\s*\{([^}]*)\}/g,(e,t,s,n)=>`if (${t}) {${n}} else {${s}}`)},{name:"early_return_removal",description:"Remove early return guards (bypass validation)",apply:e=>e.replace(/^\s*if\s*\([^)]+\)\s*\{\s*return\s*(?:null|false|undefined|0|''|"")?\s*;\s*\}\s*$/gm,"")},{name:"async_await_strip",description:"Remove await from async calls (timing/race condition potential)",apply:e=>e.replace(/const\s+(\w+)\s*=\s*await\s+/g,"const $1 = ")},{name:"todo_injection",description:"Add misleading TODO/SAFE comments near vulnerable code",apply(e){const t=["// TODO: add validation here later","// SAFE: this is sanitized upstream","// SECURITY: reviewed by team — OK","// NOTE: user input is trusted at this point"],s=Math.floor(t.length/2),n=e.split(
|
|
1
|
+
"use strict";const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),MUTATIONS=[{name:"operator_flip_eq",description:"Replace === with == (loose equality — security bypass risk)",apply:e=>e.replace(/===/g,"==").replace(/!==/g,"!=")},{name:"operator_flip_and",description:"Replace && with & (bitwise — can bypass auth checks)",apply:e=>e.replace(/if\s*\(([^)]+)\)/g,(e,t)=>"if ("+t.replace(/&&/g,"&")+")")},{name:"operator_flip_or",description:"Replace || with | (bitwise — default value bypass)",apply:e=>e.replace(/if\s*\(([^)]+)\)/g,(e,t)=>"if ("+t.replace(/\|\|/g,"|")+")")},{name:"string_concat_to_template",description:'Convert "a" + b to `a${b}` (template literal — SQL injection risk)',apply:e=>e.replace(/"([^"]{1,40}?)"\s*\+\s*(\w+)/g,(e,t,s)=>"`"+t+"${"+s+"}`")},{name:"string_split_concat",description:"Split a hardcoded string across two literals (obfuscation)",apply:e=>e.replace(/'([a-zA-Z0-9_\-]{8,24})'/g,(e,t)=>{const s=Math.floor(t.length/2);return`'${t.slice(0,s)}' + '${t.slice(s)}'`})},{name:"base64_encode_string",description:'Wrap a string value in Buffer.from().toString("base64") (obfuscation)',apply:(e,t)=>["js","ts","javascript","typescript"].includes(t)?e.replace(/'(password|secret|key|token|api_?key)':\s*'([^']+)'/gi,(e,t,s)=>`'${t}': Buffer.from('${Buffer.from(s).toString("base64")}', 'base64').toString('utf8')`):e},{name:"require_to_dynamic_import",description:"Convert require() to eval-based dynamic require (evades static analysis)",apply(e,t){if(!["js","javascript"].includes(t))return e;let s=0;return e.replace(/require\('([^']+)'\)/g,(e,t)=>s++>0?e:`eval("require")('${t}')`)}},{name:"import_alias_obfuscation",description:"Add an alias variable to obscure dangerous function names",apply(e,t){if(!["js","ts","javascript","typescript"].includes(t))return e;if(/\bexecSync\b/.test(e)){const t="_"+crypto.randomBytes(3).toString("hex");e=(e="const "+t+' = require("child_process").execSync;\n'+e).replace(/\bexecSync\b/g,t)}return e}},{name:"if_negation_flip",description:"Flip if/else branches (may invert security checks)",apply:e=>e.replace(/if\s*\(!\s*(\w+)\)\s*\{([^}]*)\}\s*else\s*\{([^}]*)\}/g,(e,t,s,n)=>`if (${t}) {${n}} else {${s}}`)},{name:"early_return_removal",description:"Remove early return guards (bypass validation)",apply:e=>e.replace(/^\s*if\s*\([^)]+\)\s*\{\s*return\s*(?:null|false|undefined|0|''|"")?\s*;\s*\}\s*$/gm,"")},{name:"async_await_strip",description:"Remove await from async calls (timing/race condition potential)",apply:e=>e.replace(/const\s+(\w+)\s*=\s*await\s+/g,"const $1 = ")},{name:"todo_injection",description:"Add misleading TODO/SAFE comments near vulnerable code",apply(e){const t=["// TODO: add validation here later","// SAFE: this is sanitized upstream","// SECURITY: reviewed by team — OK","// NOTE: user input is trusted at this point"],s=Math.floor(t.length/2),n=e.split(/\r?\n/);return n.splice(Math.min(5,n.length-1),0,t[s]),n.join("\n")}},{name:"variable_rename_obfuscate",description:"Rename obvious danger variables to innocuous names",apply(e){const t=[[/\bexecCommand\b/g,"processInput"],[/\bsqlQuery\b/g,"queryStr"],[/\brawInput\b/g,"data"],[/\buserInput\b/g,"val"],[/\bpassword\b/g,"pwd"]];for(const[s,n]of t)e=e.replace(s,n);return e}},{name:"dead_code_injection",description:"Inject plausible-looking but unreachable safe code",apply:e=>e+"\n\n// Dead code: sanitization that never runs\nfunction _sanitize(input) {\n return String(input).replace(/[<>\"';&]/g, '');\n}\n"},{name:"hex_string_encoding",description:"Encode a dangerous string value as hex (evades simple pattern matching)",apply:(e,t)=>["js","javascript","ts","typescript"].includes(t)?e.replace(/'(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)([^']{0,30})'/gi,(e,t,s)=>{const n=t+s;return`Buffer.from('${Buffer.from(n).toString("hex")}', 'hex').toString()`}):e},{name:"prototype_chain_obfuscate",description:"Use bracket notation to access __proto__ (avoids literal detection)",apply:e=>e.replace(/\.__proto__/g,'["__proto__"]').replace(/\.constructor\.prototype/g,'["constructor"]["prototype"]')},{name:"path_concat_to_join",description:"Convert path concatenation to path.join (still traversable)",apply:e=>e.replace(/(\w+)\s*\+\s*(['"]\/[^'"]+['"])/g,"path.join($1, $2)")}];class MutationEngine{constructor(e={}){this.prngseed="number"==typeof e.prngseed?e.prngseed:42,this.maxMutationsPerFile=e.maxMutationsPerFile||3,this._sessionDir=null}createSession(){if(!this._sessionDir){const e=crypto.randomUUID?crypto.randomUUID():crypto.randomBytes(16).toString("hex");this._sessionDir=path.join(os.tmpdir(),`thuban-crucible-${e}`),fs.mkdirSync(this._sessionDir,{recursive:!0})}return this._sessionDir}cleanup(){if(this._sessionDir&&fs.existsSync(this._sessionDir)){try{fs.rmSync(this._sessionDir,{recursive:!0,force:!0})}catch{}this._sessionDir=null}}_createPRNG(){let e=this.prngseed>>>0;return function(){e|=0,e=e+1831565813|0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296}}_pickMutations(e,t){const s=MUTATIONS.slice();for(let t=s.length-1;t>0;t--){const n=Math.floor(e()*(t+1));[s[t],s[n]]=[s[n],s[t]]}return s.slice(0,t)}_detectLang(e){return{".js":"js",".mjs":"js",".cjs":"js",".ts":"ts",".tsx":"ts",".py":"python",".java":"java",".cs":"csharp",".go":"go",".kt":"kotlin",".kts":"kotlin",".rs":"rust",".php":"php",".rb":"ruby"}[path.extname(e).toLowerCase()]||"unknown"}applyMutation(e,t,s="js"){const n=MUTATIONS.find(e=>e.name===t);if(!n)throw new Error(`Unknown mutation: ${t}`);try{return n.apply(e,s)}catch{return e}}applyRandomMutations(e,t="js",s){const n=null!=s?s:this.maxMutationsPerFile,a=this._createPRNG(),i=this._pickMutations(a,n),r=[];let o=e;for(const e of i)try{const s=e.apply(o,t);s!==o&&(r.push(e.name),o=s)}catch{}return{mutatedCode:o,applied:r}}mutateFile(e,t={}){const s=this.createSession(),n=this._detectLang(e),a=fs.readFileSync(e,"utf-8").replace(/\r\n/g,"\n");let i,r;if(t.only&&t.only.length){r=a,i=[];for(const e of t.only){const t=this.applyMutation(r,e,n);t!==r&&i.push(e),r=t}}else{const e=this.applyRandomMutations(a,n,t.mutations);r=e.mutatedCode,i=e.applied}const o=path.basename(e),p=path.parse(o).name,c=path.extname(o),l=`${p}-mutated-${crypto.randomBytes(3).toString("hex")}${c}`,u=path.join(s,l);return fs.writeFileSync(u,r,"utf-8"),{tempPath:u,applied:i,lang:n,original:a,mutated:r}}async mutateFiles(e,t){this.createSession();const s=[];try{for(const t of e)try{const e=this.mutateFile(t);s.push({...e,error:null})}catch(e){s.push({tempPath:null,applied:[],lang:null,error:e.message})}return await t(s)}finally{this.cleanup()}}static listMutations(){return MUTATIONS.map(e=>({name:e.name,description:e.description}))}static get mutationCount(){return MUTATIONS.length}}module.exports={MutationEngine:MutationEngine,MUTATIONS:MUTATIONS};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),COMMUNITY_FILENAME="community-discovered.json",COMMUNITY_FILE=path.join(RULES_DIR,COMMUNITY_FILENAME),MISSED_PATTERNS_FILE=path.join(__dirname,"missed-patterns.json"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;class PatternLearner{constructor(e={}){this.rulesDir=e.rulesDir||RULES_DIR,this.communityFile=e.communityFile||path.join(this.rulesDir,COMMUNITY_FILENAME),this.missedPatternsFile=e.missedPatternsFile||MISSED_PATTERNS_FILE}_readJsonArray(e){if(!fs.existsSync(e))return[];try{const t=JSON.parse(fs.readFileSync(e,"utf-8"));if(!Array.isArray(t))return[];for(const e of t)e&&"object"==typeof e&&(delete e.__proto__,delete e.constructor,delete e.prototype);return t}catch{return[]}}_writeJsonArray(e,t){fs.mkdirSync(path.dirname(e),{recursive:!0}),fs.writeFileSync(e,JSON.stringify(t,null,2)+"\n","utf-8")}_invalidateRuleCache(){try{delete require.cache[require.resolve("./rule-loader.js")]}catch{}}_nextId(e,t){if(!/^[A-Z]+$/.test(t))throw new Error(`[PatternLearner] _nextId prefix must be uppercase alpha, got: "${t}"`);let r=0;const n=new RegExp(`^${t}(\\d+)$`);for(const t of e){const e=n.exec(t.id||"");e&&(r=Math.max(r,parseInt(e[1],10)))}return t+String(r+1).padStart("MISS"===t?4:3,"0")}_deriveTrigger(e){if(!e||"string"!=typeof e)return null;const t=e.split(
|
|
1
|
+
"use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),COMMUNITY_FILENAME="community-discovered.json",COMMUNITY_FILE=path.join(RULES_DIR,COMMUNITY_FILENAME),MISSED_PATTERNS_FILE=path.join(__dirname,"missed-patterns.json"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;class PatternLearner{constructor(e={}){this.rulesDir=e.rulesDir||RULES_DIR,this.communityFile=e.communityFile||path.join(this.rulesDir,COMMUNITY_FILENAME),this.missedPatternsFile=e.missedPatternsFile||MISSED_PATTERNS_FILE}_readJsonArray(e){if(!fs.existsSync(e))return[];try{const t=JSON.parse(fs.readFileSync(e,"utf-8"));if(!Array.isArray(t))return[];for(const e of t)e&&"object"==typeof e&&(delete e.__proto__,delete e.constructor,delete e.prototype);return t}catch{return[]}}_writeJsonArray(e,t){fs.mkdirSync(path.dirname(e),{recursive:!0}),fs.writeFileSync(e,JSON.stringify(t,null,2)+"\n","utf-8")}_invalidateRuleCache(){try{delete require.cache[require.resolve("./rule-loader.js")]}catch{}}_nextId(e,t){if(!/^[A-Z]+$/.test(t))throw new Error(`[PatternLearner] _nextId prefix must be uppercase alpha, got: "${t}"`);let r=0;const n=new RegExp(`^${t}(\\d+)$`);for(const t of e){const e=n.exec(t.id||"");e&&(r=Math.max(r,parseInt(e[1],10)))}return t+String(r+1).padStart("MISS"===t?4:3,"0")}_deriveTrigger(e){if(!e||"string"!=typeof e)return null;const t=e.split(/\r?\n/).map(e=>e.trim()).filter(Boolean).find(e=>!e.startsWith("//")&&!e.startsWith("#")&&!e.startsWith("*")&&!e.startsWith("/*")&&!/^(import|from|using|package|require|namespace)\b/.test(e)&&e.length>=8&&e.length<=240);if(!t)return null;const r=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return this._checkRegexComplexity(r).safe?r:null}_checkRegexComplexity(e){if("string"!=typeof e||!e)return{safe:!1,reason:"pattern must be a non-empty string"};if(e.length>500)return{safe:!1,reason:"pattern exceeds max length of 500 characters"};let t=0,r=0;const n=[];for(let s=0;s<e.length;s++){const i=e[s];if("\\"!==i)if("("!==i){if(")"===i){const r=n.pop();if(!r)continue;const i=/^\{\d+(,\d*)?\}/.exec(e.slice(s+1)),a=e[s+1];if("+"===a||"*"===a||"?"===a||i){if(t++,r.hasQuantifier||r.hasAlternation)return{safe:!1,reason:"nested quantifier detected (potential catastrophic backtracking)"};n.length>0&&(n[n.length-1].hasQuantifier=!0)}continue}if("+"!==i&&"*"!==i&&"?"!==i){if("{"===i){/^\{\d+(,\d*)?\}/.exec(e.slice(s))&&(t++,n.length>0&&(n[n.length-1].hasQuantifier=!0));continue}"|"!==i||(r++,n.length>0&&(n[n.length-1].hasAlternation=!0))}else t++,n.length>0&&(n[n.length-1].hasQuantifier=!0)}else n.push({hasQuantifier:!1,hasAlternation:!1});else s++}return t>20?{safe:!1,reason:`pattern has ${t} quantifiers, exceeding max of 20`}:r>30?{safe:!1,reason:`pattern has ${r} alternations, exceeding max of 30`}:{safe:!0,reason:null}}patternExists(e,t){if(!fs.existsSync(this.rulesDir))return!1;const r=fs.readdirSync(this.rulesDir).filter(e=>e.endsWith(".json"));for(const n of r){const r=this._readJsonArray(path.join(this.rulesDir,n));for(const n of r)if(null==e){if(n.category===t)return!0}else if(n.pattern===e&&n.category===t)return!0}return!1}categoryCovered(e){return this.patternExists(null,e)}recordNewPattern(e={}){const{pattern:t,flags:r="gi",category:n,description:s,language:i=[],severity:a="medium",source:o}=e;if(!t||!n)throw new Error('[PatternLearner] recordNewPattern requires "pattern" and "category"');new RegExp(t,r);const d=this._checkRegexComplexity(t);if(!d.safe)throw new Error(`[PatternLearner] recordNewPattern rejected unsafe pattern: ${d.reason}`);if(this.patternExists(t,n))return{added:!1,reason:"duplicate",id:null};const c=this._readJsonArray(this.communityFile),u=this._nextId(c,"COMM"),l={id:u,pattern:t,flags:r,category:n,description:s||`Community-discovered pattern for "${n}"`,language:i,severity:a,discoveredAt:(new Date).toISOString(),source:o||"crucible-run"};return c.push(l),this._writeJsonArray(this.communityFile,c),this._invalidateRuleCache(),{added:!0,reason:null,id:u,entry:l}}recordDiscoveredIfUncovered(e={}){const{category:t,language:r,content:n,severity:s,seedId:i}=e;if(!t)return{added:!1,reason:"no-category"};if(this.categoryCovered(t))return{added:!1,reason:"already-covered"};const a=this._deriveTrigger(n);return a?this.recordNewPattern({pattern:a,flags:"gi",category:t,description:`Auto-discovered pattern for previously-uncovered category "${t}"`+(i?` (seed ${i})`:""),language:r?[r]:[],severity:s||"medium",source:`crucible-auto-discovery:${i||"unknown"}`}):{added:!1,reason:"no-derivable-trigger"}}recordMissedPattern(e={}){const{seedId:t,content:r,expectedCategory:n,language:s,scannerOutput:i,severity:a="medium",meta:o={}}=e;if(!n)throw new Error('[PatternLearner] recordMissedPattern requires "expectedCategory"');const d=this._readJsonArray(this.missedPatternsFile),c=`${t||""}|${n}|${s||""}`,u=d.find(e=>`${e.seedId||""}|${e.expectedCategory}|${e.language||""}`===c);if(u)return u.timesSeen=(u.timesSeen||1)+1,u.lastSeenAt=(new Date).toISOString(),this._writeJsonArray(this.missedPatternsFile,d),{added:!1,reason:"duplicate-updated",entry:u};const l={id:this._nextId(d,"MISS"),seedId:t||null,expectedCategory:n,language:s||null,severity:a,content:r||null,scannerOutput:i||null,status:"pending",discoveredAt:(new Date).toISOString(),timesSeen:1,meta:o};return d.push(l),this._writeJsonArray(this.missedPatternsFile,d),{added:!0,reason:null,entry:l}}getMissedPatterns(e={}){const t=this._readJsonArray(this.missedPatternsFile);return e.status?t.filter(t=>t.status===e.status):t}reviewMissedPattern(e,t){if("confirmed"!==t&&"rejected"!==t)throw new Error('[PatternLearner] decision must be "confirmed" or "rejected"');const r=this._readJsonArray(this.missedPatternsFile),n=r.find(t=>t.id===e);return n?(n.status=t,n.reviewedAt=(new Date).toISOString(),this._writeJsonArray(this.missedPatternsFile,r),n):null}promoteToScannerRule(e,t={}){const{pattern:r,flags:n="gi",description:s,language:i,severity:a}=t,o=this._readJsonArray(this.missedPatternsFile),d=o.find(t=>t.id===e);if(!d)throw new Error(`[PatternLearner] missed-pattern "${e}" not found`);if("confirmed"!==d.status)throw new Error(`[PatternLearner] missed-pattern "${e}" must be status "confirmed" before promotion (current: "${d.status}")`);if(!r)throw new Error('[PatternLearner] promoteToScannerRule requires an explicit "pattern" regex source');const c=this.recordNewPattern({pattern:r,flags:n,category:d.expectedCategory,description:s||`Promoted from missed-pattern ${e}: ${d.expectedCategory}`,language:i||(d.language?[d.language]:[]),severity:a||d.severity||"medium",source:`learn:${e}`});return c.added&&(d.status="promoted",d.promotedAt=(new Date).toISOString(),d.promotedRuleId=c.id,this._writeJsonArray(this.missedPatternsFile,o)),Object.assign({},c,{missedEntry:d})}autoPromoteMissedPatterns(){const e=this._readJsonArray(this.missedPatternsFile),t=this._readJsonArray(this.communityFile);let r=0,n=0,s=0;const i=[];for(const a of e){if("pending"!==a.status)continue;r++;const e=this._deriveTrigger(a.content);if(!e){a.status="rejected",a.reviewedAt=(new Date).toISOString(),a.reviewNote="auto-review: no derivable pattern from stored content",s++;continue}if(this.patternExists(e,a.expectedCategory)){a.status="rejected",a.reviewedAt=(new Date).toISOString(),a.reviewNote="auto-review: derived pattern already covered by an existing rule",s++;continue}a.status="confirmed",a.reviewedAt=(new Date).toISOString(),a.reviewNote="auto-confirmed: derivable pattern found";const o=this._nextId(t,"COMM");t.push({id:o,pattern:e,flags:"gi",category:a.expectedCategory,description:`Auto-promoted from missed-pattern ${a.id}: ${a.expectedCategory}`,language:a.language?[a.language]:[],severity:a.severity||"medium",discoveredAt:(new Date).toISOString(),source:`learn:${a.id}`}),a.status="promoted",a.promotedAt=(new Date).toISOString(),a.promotedRuleId=o,n++,i.push(o)}return n>0&&(this._writeJsonArray(this.communityFile,t),this._invalidateRuleCache()),r>0&&this._writeJsonArray(this.missedPatternsFile,e),{reviewed:r,promoted:n,skipped:s,promotedIds:i}}}module.exports={PatternLearner:PatternLearner,RULES_DIR:RULES_DIR,COMMUNITY_FILE:COMMUNITY_FILE,MISSED_PATTERNS_FILE:MISSED_PATTERNS_FILE};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;function _checkRegexComplexity(e){if("string"!=typeof e||!e)return{safe:!1,reason:"pattern must be a non-empty string"};if(e.length>500)return{safe:!1,reason:"pattern exceeds max length of 500"};let t=0,n=0;const r=[];for(let s=0;s<e.length;s++){const a=e[s];if("\\"!==a)if("("!==a){if(")"===a){const n=r.pop();if(!n)continue;const a=/^\{\d+(,\d*)?\}/.exec(e.slice(s+1)),i=e[s+1];if("+"===i||"*"===i||"?"===i||a){if(t++,n.hasQuantifier)return{safe:!1,reason:"nested quantifier detected (potential catastrophic backtracking)"};r.length>0&&(r[r.length-1].hasQuantifier=!0)}continue}if("+"!==a&&"*"!==a&&"?"!==a){if("{"===a){/^\{\d+(,\d*)?\}/.exec(e.slice(s))&&(t++,r.length>0&&(r[r.length-1].hasQuantifier=!0));continue}"|"!==a||(n++,r.length>0&&(r[r.length-1].hasAlternation=!0))}else t++,r.length>0&&(r[r.length-1].hasQuantifier=!0)}else{if("?"===e[s+1]){const t=e[s+2];":"===t||"="===t||"!"===t?s+=2:"<"===t&&(s+=3)}r.push({hasQuantifier:!1,hasAlternation:!1})}else s++}return t>20?{safe:!1,reason:`pattern has ${t} quantifiers, exceeding max of 20`}:n>30?{safe:!1,reason:`pattern has ${n} alternations, exceeding max of 30`}:{safe:!0,reason:null}}let _compiledPatterns=null;function getPatterns(){if(_compiledPatterns)return _compiledPatterns;const e=fs.readdirSync(RULES_DIR).filter(e=>e.endsWith(".json")).sort(),t=[];for(const n of e){const e=path.join(RULES_DIR,n);let r;try{const t=fs.readFileSync(e,"utf-8");r=JSON.parse(t)}catch(e){throw new Error(`[rule-loader] Failed to read/parse rule file "${n}": ${e.message}`)}if(!Array.isArray(r))throw new Error(`[rule-loader] Rule file "${n}" must export a JSON array, got: ${typeof r}`);for(const e of r){if(e.pattern){const t=_checkRegexComplexity(e.pattern);if(!t.safe){console.warn(`[rule-loader] WARNING: Skipping rule "${e.id}" in "${n}" — ${t.reason}`);continue}}let r,s;try{r=new RegExp(e.pattern,e.flags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile rule "${e&&e.id}" in file "${n}": ${t.message}\n pattern: ${e&&e.pattern}\n flags: ${e&&e.flags}`)}if(e.context)try{s=new RegExp(e.context,e.contextFlags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile context for rule "${e&&e.id}" in file "${n}": ${t.message}`)}t.push({regex:r,category:e.category,id:e.id,description:e.description,language:e.language,severity:e.severity,name:e.name,type:e.type,message:e.message,context:s,skipIfSafe:e.skipIfSafe,skipInTests:e.skipInTests,excludeExtensions:e.excludeExtensions,sourceFile:n})}}return _compiledPatterns=t,_compiledPatterns}function getCodeScannerPatterns(){return getPatterns().filter(e=>"code-scanner-core.json"===e.sourceFile).map(e=>{const t={id:e.id,name:e.name,type:e.type,pattern:e.regex,severity:e.severity,message:e.message};return e.context&&(t.context=e.context),e.skipIfSafe&&(t.skipIfSafe=!0),e.skipInTests&&(t.skipInTests=!0),e.excludeExtensions&&(t.excludeExtensions=e.excludeExtensions),t})}module.exports={getPatterns:getPatterns,getCodeScannerPatterns:getCodeScannerPatterns};
|
|
1
|
+
"use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;function _checkRegexComplexity(e){if("string"!=typeof e||!e)return{safe:!1,reason:"pattern must be a non-empty string"};if(e.length>500)return{safe:!1,reason:"pattern exceeds max length of 500"};let t=0,n=0;const r=[];for(let s=0;s<e.length;s++){const a=e[s];if("\\"!==a)if("("!==a){if(")"===a){const n=r.pop();if(!n)continue;const a=/^\{\d+(,\d*)?\}/.exec(e.slice(s+1)),i=e[s+1];if("+"===i||"*"===i||"?"===i||a){if(t++,n.hasQuantifier)return{safe:!1,reason:"nested quantifier detected (potential catastrophic backtracking)"};r.length>0&&(r[r.length-1].hasQuantifier=!0)}continue}if("+"!==a&&"*"!==a&&"?"!==a){if("{"===a){/^\{\d+(,\d*)?\}/.exec(e.slice(s))&&(t++,r.length>0&&(r[r.length-1].hasQuantifier=!0));continue}"|"!==a||(n++,r.length>0&&(r[r.length-1].hasAlternation=!0))}else t++,r.length>0&&(r[r.length-1].hasQuantifier=!0)}else{if("?"===e[s+1]){const t=e[s+2];":"===t||"="===t||"!"===t?s+=2:"<"===t&&(s+=3)}r.push({hasQuantifier:!1,hasAlternation:!1})}else s++}return t>20?{safe:!1,reason:`pattern has ${t} quantifiers, exceeding max of 20`}:n>30?{safe:!1,reason:`pattern has ${n} alternations, exceeding max of 30`}:{safe:!0,reason:null}}let _compiledPatterns=null;function getPatterns(){if(_compiledPatterns)return _compiledPatterns;const e=fs.readdirSync(RULES_DIR).filter(e=>e.endsWith(".json")).sort(),t=[];for(const n of e){const e=path.join(RULES_DIR,n);let r;try{const t=fs.readFileSync(e,"utf-8");r=JSON.parse(t)}catch(e){throw new Error(`[rule-loader] Failed to read/parse rule file "${n}": ${e.message}`)}if(!Array.isArray(r))throw new Error(`[rule-loader] Rule file "${n}" must export a JSON array, got: ${typeof r}`);for(const e of r){if(e.pattern){const t=_checkRegexComplexity(e.pattern);if(!t.safe){console.warn(`[rule-loader] WARNING: Skipping rule "${e.id}" in "${n}" — ${t.reason}`);continue}}let r,s;try{r=new RegExp(e.pattern,e.flags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile rule "${e&&e.id}" in file "${n}": ${t.message}\n pattern: ${e&&e.pattern}\n flags: ${e&&e.flags}`)}if(e.context)try{s=new RegExp(e.context,e.contextFlags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile context for rule "${e&&e.id}" in file "${n}": ${t.message}`)}t.push({regex:r,category:e.category,id:e.id,description:e.description,language:e.language,severity:e.severity,name:e.name,type:e.type,message:e.message,context:s,skipIfSafe:e.skipIfSafe,skipInTests:e.skipInTests,excludeExtensions:e.excludeExtensions,owasp:e.owasp,cwe:e.cwe,sourceFile:n})}}return _compiledPatterns=t,_compiledPatterns}function getCodeScannerPatterns(){return getPatterns().filter(e=>"code-scanner-core.json"===e.sourceFile).map(e=>{const t={id:e.id,name:e.name,type:e.type,pattern:e.regex,severity:e.severity,message:e.message};return e.context&&(t.context=e.context),e.skipIfSafe&&(t.skipIfSafe=!0),e.skipInTests&&(t.skipInTests=!0),e.excludeExtensions&&(t.excludeExtensions=e.excludeExtensions),e.owasp&&(t.owasp=e.owasp),e.cwe&&(t.cwe=e.cwe),t})}module.exports={getPatterns:getPatterns,getCodeScannerPatterns:getCodeScannerPatterns};
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"message": "Possible hardcoded API key or secret detected",
|
|
10
10
|
"context": "(api[_-]?key|apikey|secret[_\\s]*key|token|credential|bearer)",
|
|
11
11
|
"contextFlags": "i",
|
|
12
|
-
"skipIfSafe": true
|
|
12
|
+
"skipIfSafe": true,
|
|
13
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
14
|
+
"cwe": "CWE-798"
|
|
13
15
|
},
|
|
14
16
|
{
|
|
15
17
|
"id": "SEC001B",
|
|
@@ -19,7 +21,9 @@
|
|
|
19
21
|
"flags": "",
|
|
20
22
|
"severity": "critical",
|
|
21
23
|
"message": "Possible hardcoded API key or secret detected",
|
|
22
|
-
"skipIfSafe": true
|
|
24
|
+
"skipIfSafe": true,
|
|
25
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
26
|
+
"cwe": "CWE-798"
|
|
23
27
|
},
|
|
24
28
|
{
|
|
25
29
|
"id": "SEC002",
|
|
@@ -30,7 +34,9 @@
|
|
|
30
34
|
"severity": "critical",
|
|
31
35
|
"message": "Hardcoded password detected",
|
|
32
36
|
"skipIfSafe": true,
|
|
33
|
-
"skipInTests": true
|
|
37
|
+
"skipInTests": true,
|
|
38
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
39
|
+
"cwe": "CWE-798"
|
|
34
40
|
},
|
|
35
41
|
{
|
|
36
42
|
"id": "SEC002B",
|
|
@@ -41,7 +47,9 @@
|
|
|
41
47
|
"severity": "critical",
|
|
42
48
|
"message": "Hardcoded password detected",
|
|
43
49
|
"skipIfSafe": true,
|
|
44
|
-
"skipInTests": true
|
|
50
|
+
"skipInTests": true,
|
|
51
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
52
|
+
"cwe": "CWE-798"
|
|
45
53
|
},
|
|
46
54
|
{
|
|
47
55
|
"id": "SEC003",
|
|
@@ -50,7 +58,9 @@
|
|
|
50
58
|
"pattern": "\\b(?:SELECT|INSERT\\s+INTO|UPDATE|DELETE\\s+FROM|WHERE)\\b.*(\\$\\{|\\+\\s*['\"]|\\+\\s*\\w+|f['\"].*\\{|%s|%d)",
|
|
51
59
|
"flags": "i",
|
|
52
60
|
"severity": "error",
|
|
53
|
-
"message": "Potential SQL injection - use parameterized queries"
|
|
61
|
+
"message": "Potential SQL injection - use parameterized queries",
|
|
62
|
+
"owasp": "A03:2021 Injection",
|
|
63
|
+
"cwe": "CWE-89"
|
|
54
64
|
},
|
|
55
65
|
{
|
|
56
66
|
"id": "SEC004",
|
|
@@ -59,7 +69,9 @@
|
|
|
59
69
|
"pattern": "\\beval\\s*\\(",
|
|
60
70
|
"flags": "",
|
|
61
71
|
"severity": "error",
|
|
62
|
-
"message": "eval() is dangerous and should be avoided"
|
|
72
|
+
"message": "eval() is dangerous and should be avoided",
|
|
73
|
+
"owasp": "A03:2021 Injection",
|
|
74
|
+
"cwe": "CWE-95"
|
|
63
75
|
},
|
|
64
76
|
{
|
|
65
77
|
"id": "SEC004B",
|
|
@@ -68,7 +80,9 @@
|
|
|
68
80
|
"pattern": "\\\\u0065\\\\u0076\\\\u0061\\\\u006[cC]",
|
|
69
81
|
"flags": "",
|
|
70
82
|
"severity": "error",
|
|
71
|
-
"message": "Obfuscated eval() usage (unicode escapes) is dangerous and should be avoided"
|
|
83
|
+
"message": "Obfuscated eval() usage (unicode escapes) is dangerous and should be avoided",
|
|
84
|
+
"owasp": "A03:2021 Injection",
|
|
85
|
+
"cwe": "CWE-95"
|
|
72
86
|
},
|
|
73
87
|
{
|
|
74
88
|
"id": "SEC005",
|
|
@@ -77,7 +91,9 @@
|
|
|
77
91
|
"pattern": "\\.innerHTML\\s*=|\\bdocument\\.write\\s*\\(",
|
|
78
92
|
"flags": "",
|
|
79
93
|
"severity": "warning",
|
|
80
|
-
"message": "innerHTML/document.write can cause XSS - consider using textContent or sanitization"
|
|
94
|
+
"message": "innerHTML/document.write can cause XSS - consider using textContent or sanitization",
|
|
95
|
+
"owasp": "A03:2021 Injection",
|
|
96
|
+
"cwe": "CWE-79"
|
|
81
97
|
},
|
|
82
98
|
{
|
|
83
99
|
"id": "SEC006",
|
|
@@ -86,7 +102,9 @@
|
|
|
86
102
|
"pattern": "rejectUnauthorized\\s*:\\s*false|verify\\s*=\\s*False|InsecureSkipVerify\\s*:\\s*true",
|
|
87
103
|
"flags": "",
|
|
88
104
|
"severity": "error",
|
|
89
|
-
"message": "SSL certificate validation disabled"
|
|
105
|
+
"message": "SSL certificate validation disabled",
|
|
106
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
107
|
+
"cwe": "CWE-295"
|
|
90
108
|
},
|
|
91
109
|
{
|
|
92
110
|
"id": "SEC007",
|
|
@@ -97,7 +115,9 @@
|
|
|
97
115
|
"severity": "warning",
|
|
98
116
|
"message": "Direct .env file manipulation - ensure not exposed",
|
|
99
117
|
"context": "fs\\.(read|write)|path\\.join|open\\s*\\(|os\\.Open",
|
|
100
|
-
"contextFlags": "i"
|
|
118
|
+
"contextFlags": "i",
|
|
119
|
+
"owasp": "A05:2021 Security Misconfiguration",
|
|
120
|
+
"cwe": "CWE-538"
|
|
101
121
|
},
|
|
102
122
|
{
|
|
103
123
|
"id": "SEC008",
|
|
@@ -106,7 +126,9 @@
|
|
|
106
126
|
"pattern": "\\bunsafe\\s*\\{",
|
|
107
127
|
"flags": "",
|
|
108
128
|
"severity": "warning",
|
|
109
|
-
"message": "Rust unsafe block - verify memory safety guarantees"
|
|
129
|
+
"message": "Rust unsafe block - verify memory safety guarantees",
|
|
130
|
+
"owasp": "A06:2021 Vulnerable and Outdated Components",
|
|
131
|
+
"cwe": "CWE-787"
|
|
110
132
|
},
|
|
111
133
|
{
|
|
112
134
|
"id": "SEC009",
|
|
@@ -116,7 +138,9 @@
|
|
|
116
138
|
"flags": "",
|
|
117
139
|
"severity": "error",
|
|
118
140
|
"message": "Shell/OS execution function — command injection risk if input unsanitized",
|
|
119
|
-
"excludeExtensions": [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]
|
|
141
|
+
"excludeExtensions": [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"],
|
|
142
|
+
"owasp": "A03:2021 Injection",
|
|
143
|
+
"cwe": "CWE-78"
|
|
120
144
|
},
|
|
121
145
|
{
|
|
122
146
|
"id": "SEC010",
|
|
@@ -125,7 +149,9 @@
|
|
|
125
149
|
"pattern": "\\[\\s*['\"]__proto__['\"]\\s*\\]|\\.__proto__\\s*(=|\\[)|constructor\\s*\\[\\s*['\"]prototype['\"]\\s*\\]|constructor\\.prototype\\s*(=|\\[)|['\"]__proto__['\"]\\s*:",
|
|
126
150
|
"flags": "",
|
|
127
151
|
"severity": "error",
|
|
128
|
-
"message": "Possible prototype pollution via __proto__/constructor.prototype"
|
|
152
|
+
"message": "Possible prototype pollution via __proto__/constructor.prototype",
|
|
153
|
+
"owasp": "A03:2021 Injection",
|
|
154
|
+
"cwe": "CWE-1321"
|
|
129
155
|
},
|
|
130
156
|
{
|
|
131
157
|
"id": "SEC011",
|
|
@@ -134,7 +160,9 @@
|
|
|
134
160
|
"pattern": "\"SELECT[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"UPDATE[^\"]*SET[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"DELETE[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\\$(?:query|sql|qry)\\s*\\.=\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE)",
|
|
135
161
|
"flags": "i",
|
|
136
162
|
"severity": "critical",
|
|
137
|
-
"message": "PHP SQL injection: user input concatenated into SQL query with dot operator — use PDO prepared statements"
|
|
163
|
+
"message": "PHP SQL injection: user input concatenated into SQL query with dot operator — use PDO prepared statements",
|
|
164
|
+
"owasp": "A03:2021 Injection",
|
|
165
|
+
"cwe": "CWE-89"
|
|
138
166
|
},
|
|
139
167
|
{
|
|
140
168
|
"id": "SEC012",
|
|
@@ -143,7 +171,9 @@
|
|
|
143
171
|
"pattern": "echo\\s+(?!htmlspecialchars|htmlentities|strip_tags|esc_html).*\\$(?:_GET|_POST|_REQUEST|_COOKIE)|echo\\s+[\"'][^\"']*[\"']\\s*\\.\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE)|print\\s+\\$(?:_GET|_POST|_REQUEST|_COOKIE)",
|
|
144
172
|
"flags": "i",
|
|
145
173
|
"severity": "error",
|
|
146
|
-
"message": "PHP XSS: unescaped user input echoed to page — use htmlspecialchars()"
|
|
174
|
+
"message": "PHP XSS: unescaped user input echoed to page — use htmlspecialchars()",
|
|
175
|
+
"owasp": "A03:2021 Injection",
|
|
176
|
+
"cwe": "CWE-79"
|
|
147
177
|
},
|
|
148
178
|
{
|
|
149
179
|
"id": "SEC013",
|
|
@@ -154,7 +184,9 @@
|
|
|
154
184
|
"severity": "critical",
|
|
155
185
|
"message": "Hardcoded JWT token detected — rotate immediately and use environment variables",
|
|
156
186
|
"skipIfSafe": true,
|
|
157
|
-
"skipInTests": true
|
|
187
|
+
"skipInTests": true,
|
|
188
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
189
|
+
"cwe": "CWE-798"
|
|
158
190
|
},
|
|
159
191
|
{
|
|
160
192
|
"id": "SEC014",
|
|
@@ -163,7 +195,9 @@
|
|
|
163
195
|
"pattern": "-----BEGIN (?:RSA |EC )?PRIVATE KEY-----",
|
|
164
196
|
"flags": "",
|
|
165
197
|
"severity": "critical",
|
|
166
|
-
"message": "Private key embedded in source code — move to secure key store or environment variable"
|
|
198
|
+
"message": "Private key embedded in source code — move to secure key store or environment variable",
|
|
199
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
200
|
+
"cwe": "CWE-321"
|
|
167
201
|
},
|
|
168
202
|
{
|
|
169
203
|
"id": "SEC015",
|
|
@@ -173,7 +207,9 @@
|
|
|
173
207
|
"flags": "",
|
|
174
208
|
"severity": "critical",
|
|
175
209
|
"message": "AWS Secret Access Key hardcoded — use IAM roles or environment variables",
|
|
176
|
-
"skipIfSafe": true
|
|
210
|
+
"skipIfSafe": true,
|
|
211
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
212
|
+
"cwe": "CWE-798"
|
|
177
213
|
},
|
|
178
214
|
{
|
|
179
215
|
"id": "SEC016",
|
|
@@ -183,7 +219,9 @@
|
|
|
183
219
|
"flags": "i",
|
|
184
220
|
"severity": "critical",
|
|
185
221
|
"message": "Hardcoded secret in Rust const — use environment variables at runtime",
|
|
186
|
-
"skipInTests": true
|
|
222
|
+
"skipInTests": true,
|
|
223
|
+
"owasp": "A02:2021 Cryptographic Failures",
|
|
224
|
+
"cwe": "CWE-798"
|
|
187
225
|
},
|
|
188
226
|
{
|
|
189
227
|
"id": "SEC017",
|
|
@@ -192,7 +230,9 @@
|
|
|
192
230
|
"pattern": "format!\\s*\\(\\s*\"(?:SELECT|INSERT|UPDATE|DELETE|WHERE)[^\"]*\\{",
|
|
193
231
|
"flags": "i",
|
|
194
232
|
"severity": "error",
|
|
195
|
-
"message": "SQL query built with format!() macro — use parameterized queries (sqlx::query! or bind)"
|
|
233
|
+
"message": "SQL query built with format!() macro — use parameterized queries (sqlx::query! or bind)",
|
|
234
|
+
"owasp": "A03:2021 Injection",
|
|
235
|
+
"cwe": "CWE-89"
|
|
196
236
|
},
|
|
197
237
|
{
|
|
198
238
|
"id": "SEC018",
|
|
@@ -201,7 +241,9 @@
|
|
|
201
241
|
"pattern": "[\"'](?:SELECT|INSERT|UPDATE|DELETE|WHERE|<)[^\"']*#\\{[^}]*(?:params|request|input|user|cookies|session)[^}]*\\}",
|
|
202
242
|
"flags": "i",
|
|
203
243
|
"severity": "error",
|
|
204
|
-
"message": "User input interpolated via Ruby #{} into SQL/HTML — use parameterized queries or sanitize"
|
|
244
|
+
"message": "User input interpolated via Ruby #{} into SQL/HTML — use parameterized queries or sanitize",
|
|
245
|
+
"owasp": "A03:2021 Injection",
|
|
246
|
+
"cwe": "CWE-89"
|
|
205
247
|
},
|
|
206
248
|
{
|
|
207
249
|
"id": "SEC019",
|
|
@@ -210,7 +252,9 @@
|
|
|
210
252
|
"pattern": "fmt\\.Sprintf\\s*\\(\\s*\"(?:SELECT|INSERT|UPDATE|DELETE|WHERE)[^\"]*%[sv]",
|
|
211
253
|
"flags": "i",
|
|
212
254
|
"severity": "error",
|
|
213
|
-
"message": "SQL query built with fmt.Sprintf — use parameterized queries (db.Query with $1/? placeholders)"
|
|
255
|
+
"message": "SQL query built with fmt.Sprintf — use parameterized queries (db.Query with $1/? placeholders)",
|
|
256
|
+
"owasp": "A03:2021 Injection",
|
|
257
|
+
"cwe": "CWE-89"
|
|
214
258
|
},
|
|
215
259
|
{
|
|
216
260
|
"id": "RUST001",
|
|
@@ -219,7 +263,9 @@
|
|
|
219
263
|
"pattern": "mem::uninitialized\\s*\\(|MaybeUninit::uninit\\(\\)\\.assume_init\\(\\)",
|
|
220
264
|
"flags": "",
|
|
221
265
|
"severity": "error",
|
|
222
|
-
"message": "mem::uninitialized is UB since Rust 1.39 — use MaybeUninit::zeroed() or MaybeUninit with proper init"
|
|
266
|
+
"message": "mem::uninitialized is UB since Rust 1.39 — use MaybeUninit::zeroed() or MaybeUninit with proper init",
|
|
267
|
+
"owasp": "A06:2021 Vulnerable and Outdated Components",
|
|
268
|
+
"cwe": "CWE-908"
|
|
223
269
|
},
|
|
224
270
|
{
|
|
225
271
|
"id": "RUST002",
|
|
@@ -228,6 +274,8 @@
|
|
|
228
274
|
"pattern": "\\.trim_left\\(\\)|\\.trim_right\\(\\)|\\.trim_left_matches\\(|\\.trim_right_matches\\(",
|
|
229
275
|
"flags": "",
|
|
230
276
|
"severity": "warning",
|
|
231
|
-
"message": "trim_left/trim_right deprecated since Rust 1.33 — use trim_start/trim_end"
|
|
277
|
+
"message": "trim_left/trim_right deprecated since Rust 1.33 — use trim_start/trim_end",
|
|
278
|
+
"owasp": "A06:2021 Vulnerable and Outdated Components",
|
|
279
|
+
"cwe": "CWE-477"
|
|
232
280
|
}
|
|
233
281
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const fs=require("fs"),path=require("path");function mulberry32(e){let t=e>>>0;return function(){t|=0,t=t+1831565813|0;let e=Math.imul(t^t>>>15,1|t);return e=e+Math.imul(e^e>>>7,61|e)^e,((e^e>>>14)>>>0)/4294967296}}const LANG_DIR_MAP={javascript:"js",js:"js",typescript:"ts",ts:"ts",python:"python",py:"python",java:"java",csharp:"csharp",cs:"csharp",go:"go",golang:"go",kotlin:"kotlin",kt:"kotlin",rust:"rust",rs:"rust",php:"php",ruby:"ruby",rb:"ruby"},ALL_LANGUAGES=["js","ts","python","java","csharp","go","kotlin","rust","php","ruby"];function parseSeedHeader(e){const t=e.split(
|
|
1
|
+
"use strict";const fs=require("fs"),path=require("path");function mulberry32(e){let t=e>>>0;return function(){t|=0,t=t+1831565813|0;let e=Math.imul(t^t>>>15,1|t);return e=e+Math.imul(e^e>>>7,61|e)^e,((e^e>>>14)>>>0)/4294967296}}const LANG_DIR_MAP={javascript:"js",js:"js",typescript:"ts",ts:"ts",python:"python",py:"python",java:"java",csharp:"csharp",cs:"csharp",go:"go",golang:"go",kotlin:"kotlin",kt:"kotlin",rust:"rust",rs:"rust",php:"php",ruby:"ruby",rb:"ruby"},ALL_LANGUAGES=["js","ts","python","java","csharp","go","kotlin","rust","php","ruby"];function parseSeedHeader(e){const t=e.split(/\r?\n/).slice(0,20),s={};let r=!1;for(const e of t){const t=e.trim().replace(/^\/\/\s*/,"").replace(/^#\s*/,"").replace(/^\*\s*/,"");if("[CRUCIBLE-SEED]"===t){r=!0;continue}if(!r)continue;const a=t.match(/^(category|severity|language|expected):\s*(.+)$/);if(a&&(s[a[1]]=a[2].trim()),Object.keys(s).length>=4)break}return Object.keys(s).length>=3?s:null}class SeedingEngine{constructor(e={}){this.seedsDir=e.seedsDir||path.join(__dirname,"seeds"),this.prngseed="number"==typeof e.prngseed?e.prngseed:42,this._byLanguage=new Map,this._byCategory=new Map,this._all=[],this._loaded=!1}load(){if(this._loaded)return this._all.length;if(!fs.existsSync(this.seedsDir))throw new Error(`Seeds directory not found: ${this.seedsDir}`);const e=fs.readdirSync(this.seedsDir,{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name);for(const t of e){const e=path.join(this.seedsDir,t),s=fs.readdirSync(e).filter(e=>/\.(js|ts|py|java|cs|go|kt|rs|php|rb)$/.test(e)).sort();for(const r of s){const s=path.join(e,r);let a;try{a=fs.readFileSync(s,"utf-8").replace(/\r\n/g,"\n")}catch{continue}const n=parseSeedHeader(a),i={id:`${t}/${r}`,file:s,langDir:t,filename:r,content:a,category:n?.category||"unknown",severity:n?.severity||"medium",language:n?.language||t,expected:n?.expected||""};this._all.push(i),this._byLanguage.has(t)||this._byLanguage.set(t,[]),this._byLanguage.get(t).push(i);const o=i.category;this._byCategory.has(o)||this._byCategory.set(o,[]),this._byCategory.get(o).push(i)}}return this._loaded=!0,this._all.length}get languages(){return this.load(),Array.from(this._byLanguage.keys()).sort()}get categories(){return this.load(),Array.from(this._byCategory.keys()).sort()}get count(){return this.load(),this._all.length}forLanguage(e){this.load();const t=LANG_DIR_MAP[e.toLowerCase()]||e.toLowerCase();return this._byLanguage.get(t)||[]}forCategory(e){return this.load(),this._byCategory.get(e)||[]}forLanguageAndCategory(e,t){return this.load(),this.forLanguage(e).filter(e=>e.category===t)}createPRNG(){return mulberry32(this.prngseed)}_shuffle(e,t){for(let s=e.length-1;s>0;s--){const r=Math.floor(t()*(s+1));[e[s],e[r]]=[e[r],e[s]]}return e}sample(e,t){const s=this.createPRNG(),r=e.slice();return this._shuffle(r,s),r.slice(0,t)}selectForLevel(e,t={}){this.load();let s=this._all.slice();if(t.languages&&t.languages.length){const e=t.languages.map(e=>LANG_DIR_MAP[e.toLowerCase()]||e.toLowerCase());s=s.filter(t=>e.includes(t.langDir))}t.categories&&t.categories.length&&(s=s.filter(e=>t.categories.includes(e.category)));const r=this.createPRNG(),a=s.slice();switch(this._shuffle(a,r),e){case"gentle":default:return a.slice(0,50);case"medium":return a.slice(0,200);case"hell":return a}}stats(){this.load();const e={};for(const[t,s]of this._byLanguage)e[t]=s.length;const t={};for(const[e,s]of this._byCategory)t[e]=s.length;const s={critical:0,high:0,medium:0,low:0};for(const e of this._all){const t=(e.severity||"medium").toLowerCase();s[t]=(s[t]||0)+1}return{total:this._all.length,byLanguage:e,byCategory:t,bySeverity:s}}}module.exports={SeedingEngine:SeedingEngine,mulberry32:mulberry32,parseSeedHeader:parseSeedHeader,ALL_LANGUAGES:ALL_LANGUAGES,LANG_DIR_MAP:LANG_DIR_MAP};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const fs=require("fs"),path=require("path"),HallucinationDetector=require("./hallucination-detector.js"),RISK_SIGNALS={evalUsage:/\beval\s*\(/,hardcodedSecret:/['"](?:sk-|pk-|xox[pboa]-|AKIA|ghp_|gho_|github_pat_)[a-zA-Z0-9_-]{10,}['"]|['"][a-zA-Z0-9]{32,}['"]|password\s*[:=]\s*['"][^'"]+['"]|["']password["']\s*:\s*["'][^"']+["']/i,commandInjection:/\b(?:child_process\.)?(?:exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(\s*(?:`[^`]*\$\{|[^,)]*\+\s*\w|[^,)]*\+\s*['"`])/,sqlInjection:/\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WHERE)\b.*(\$\{|\+\s*['"]|\+\s*\w+|f['"].*\{|%s|%d)/i},AI_SIGNALS={genericNames:["data","result","response","output","input","temp","item","element","val","obj","arr","str","num","flag","status"],aiCommentPatterns:[/\/\/\s*TODO:?\s*implement/i,/\/\/\s*handle\s+error/i,/\/\/\s*add\s+error\s+handling/i,/\/\/\s*you\s+(can|may|might|should)/i,/\/\/\s*replace\s+(this|with)/i,/\/\/\s*this\s+(function|method|class)\s+(takes|accepts|returns|handles)/i,/\/\/\s*example\s+usage/i,/\/\/\s*note:?\s*this/i,/\/\*\*[\s\S]*?@description\s/,/\/\/\s*import.*if\s+needed/i],aiStructurePatterns:[/try\s*\{[\s\S]*?\}\s*catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]*?console\.(log|error)\s*\(\s*(?:error|err|e)\s*\)/,/if\s*\(!.*\)\s*\{?\s*(?:return|throw)\s/]};class AIConfidenceScorer{constructor(e={}){this.rootPath=e.rootPath||process.cwd(),this.hallDetector=e.hallucinationDetector||new HallucinationDetector({rootPath:this.rootPath})}async scan(e){const t=[];let s=new Map;try{const t=await this.hallDetector.scan(e),n=[...t.phantomAPIs||[],...t.deprecatedAPIs||[]];for(const e of n)s.has(e.file)||s.set(e.file,[]),s.get(e.file).push(e)}catch(e){}for(const n of e){let e;try{if(fs.statSync(n).size>524288)continue;e=fs.readFileSync(n,"utf-8")}catch(e){continue}const i=path.relative(this.rootPath,n);if(this._isSkippable(i))continue;const c=s.get(i)||[],r=this._extractFunctions(e,i,c);t.push(...r)}t.sort((e,t)=>t.confidence-e.confidence);const n=t.filter(e=>e.confidence>=70),i=t.filter(e=>e.confidence>=40&&e.confidence<70),c=t.filter(e=>e.riskScore>=70),r=t.filter(e=>e.riskScore>=40&&e.riskScore<70);return{functions:t,highConfidence:n,mediumConfidence:i,totalFunctions:t.length,aiLikelyCount:n.length,aiPossibleCount:i.length,aiPercentage:t.length>0?Math.round(n.length/t.length*100):0,highRisk:c,mediumRisk:r,highRiskCount:c.length,mediumRiskCount:r.length}}_extractFunctions(e,t,s=[]){const n=e.split("\n"),i=[];for(let e=0;e<n.length;e++){const c=this._detectFunction(n,e);if(!c)continue;const r=this._findFunctionEnd(n,e),o=n.slice(e,r+1).join("\n"),a=n.slice(e,r+1),l=e+1,h=r+1,u=this._scoreFunction(c.name,o,a,n,e),m=this._scoreRisk(o,a,s,l,h);i.push({file:t,name:c.name,line:e+1,lineCount:r-e+1,confidence:u.score,signals:u.reasons,riskScore:m.score,riskSignals:m.reasons,riskSeverity:m.score>=70?"critical":m.score>=40?"high":m.score>=15?"medium":"low",testCoverage:this._hasTests(c.name,t)?"found":"none",category:"ai_confidence",severity:u.score>=80?"high":u.score>=50?"medium":"low"}),e=r}return i}_scoreRisk(e,t,s,n,i){let c=0;const r=[];RISK_SIGNALS.evalUsage.test(e)&&(c+=40,r.push("eval() usage — arbitrary code execution risk"));const o=s.filter(e=>e.line>=n&&e.line<=i);o.length>0&&(c+=Math.min(30,15*o.length),r.push(`${o.length} hallucinated/deprecated API usage(s): ${o.map(e=>e.name).slice(0,3).join(", ")}`));const a=this._maxBraceDepth(t);a>3&&(c+=Math.min(20,8*(a-3)),r.push(`Deep nesting — ${a} levels (>3 is risky)`));const l=this._cyclomaticComplexity(e);return l>10&&(c+=Math.min(15,Math.round(1.5*(l-10))),r.push(`High cyclomatic complexity (${l})`)),RISK_SIGNALS.hardcodedSecret.test(e)&&(c+=25,r.push("Hardcoded secret/credential detected")),RISK_SIGNALS.commandInjection.test(e)&&(c+=25,r.push("Command injection risk — exec/spawn with unsanitized string concatenation")),RISK_SIGNALS.sqlInjection.test(e)&&(c+=25,r.push("SQL injection risk — query built via string concatenation/interpolation")),c=Math.min(100,c),{score:c,reasons:r}}_maxBraceDepth(e){let t=0,s=0;for(const n of e)for(const e of n)"{"===e?(t++,t>s&&(s=t)):"}"===e&&(t=Math.max(0,t-1));return Math.max(0,s-1)}_cyclomaticComplexity(e){return 1+(e.match(/\b(if|else if|for|while|case|catch)\b|\?\s*[^:]+:|&&|\|\|/g)||[]).length}_scoreFunction(e,t,s,n,i){let c=0;const r=[],o=AI_SIGNALS.genericNames.filter(e=>{const s=new RegExp(`\\b${e}\\b`,"g");return(t.match(s)||[]).length>=2}).length;o>=3?(c+=15,r.push(`${o} generic variable names (data, result, response...)`)):o>=1&&(c+=8,r.push(`${o} generic variable name(s)`));const a=AI_SIGNALS.aiCommentPatterns.filter(e=>e.test(t)).length;if(a>=2?(c+=20,r.push(`${a} AI-style comments (TODO: implement, handle error, etc.)`)):a>=1&&(c+=10,r.push("AI-style comment detected")),i>0){const e=n.slice(Math.max(0,i-10),i).join("\n");/\/\*\*[\s\S]*@param[\s\S]*@returns?[\s\S]*@(?:throws|example)/s.test(e)&&(c+=10,r.push("Exhaustive JSDoc (params + returns + throws/example)"))}(t.match(/try\s*\{/g)||[]).length>=2&&(c+=10,r.push("Multiple try/catch blocks (AI over-handles errors)"));const l=s.length;return l>=50?(c+=15,r.push(`${l} lines — large self-contained function`)):l>=25&&(c+=8,r.push(`${l} lines`)),0===n.slice(0,i).concat(n.slice(i+l)).filter(t=>t.includes(e)).length&&l>10&&(c+=10,r.push("Isolated — no internal references")),/catch\s*\([^)]*\)\s*\{[^}]*console\.(log|error)\s*\(/.test(t)&&(c+=8,r.push("console.log/error in catch block (AI default pattern)")),/=>\s*\{[\s\n\r]*const\s*\{/.test(t)&&(c+=5,r.push("Immediate destructuring after arrow (common AI pattern)")),(t.match(/`[^`]*\$\{/g)||[]).length>=4&&(c+=5,r.push("Heavy template literal usage")),c=Math.min(100,c),{score:c,reasons:r}}_detectFunction(e,t){const s=e[t].trim();let n=s.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=s.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n&&(s.includes("=>")||s.includes("function"))?{name:n[1]}:(n=s.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=s.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=s.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=s.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=s.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),n?{name:n[1]}:null))))))}_findFunctionEnd(e,t){const s=(e[t]||"").trim();if(/^def\s/.test(s)){let s=1;for(let n=t+1;n<e.length&&n<t+300;n++){const t=e[n].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)return n}return Math.min(t+20,e.length-1)}let n=0,i=!1;for(let s=t;s<e.length&&s<t+300;s++){for(const t of e[s])"{"===t&&(n++,i=!0),"}"===t&&n--;if(i&&n<=0)return s}return Math.min(t+20,e.length-1)}_hasTests(e,t){return[t.replace(/\.(js|ts)$/,".test.$1"),t.replace(/\.(js|ts)$/,".spec.$1"),t.replace(/src\//,"test/"),t.replace(/src\//,"__tests__/")].some(e=>{try{return fs.existsSync(path.resolve(this.rootPath,e))}catch(e){return!1}})}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/node_modules/i,/\.min\./i].some(t=>t.test(e))}}module.exports=AIConfidenceScorer;
|
|
1
|
+
const fs=require("fs"),path=require("path"),HallucinationDetector=require("./hallucination-detector.js"),RISK_SIGNALS={evalUsage:/\beval\s*\(/,hardcodedSecret:/['"](?:sk-|pk-|xox[pboa]-|AKIA|ghp_|gho_|github_pat_)[a-zA-Z0-9_-]{10,}['"]|['"][a-zA-Z0-9]{32,}['"]|password\s*[:=]\s*['"][^'"]+['"]|["']password["']\s*:\s*["'][^"']+["']/i,commandInjection:/\b(?:child_process\.)?(?:exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(\s*(?:`[^`]*\$\{|[^,)]*\+\s*\w|[^,)]*\+\s*['"`])/,sqlInjection:/\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WHERE)\b.*(\$\{|\+\s*['"]|\+\s*\w+|f['"].*\{|%s|%d)/i},AI_SIGNALS={genericNames:["data","result","response","output","input","temp","item","element","val","obj","arr","str","num","flag","status"],aiCommentPatterns:[/\/\/\s*TODO:?\s*implement/i,/\/\/\s*handle\s+error/i,/\/\/\s*add\s+error\s+handling/i,/\/\/\s*you\s+(can|may|might|should)/i,/\/\/\s*replace\s+(this|with)/i,/\/\/\s*this\s+(function|method|class)\s+(takes|accepts|returns|handles)/i,/\/\/\s*example\s+usage/i,/\/\/\s*note:?\s*this/i,/\/\*\*[\s\S]*?@description\s/,/\/\/\s*import.*if\s+needed/i],aiStructurePatterns:[/try\s*\{[\s\S]*?\}\s*catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]*?console\.(log|error)\s*\(\s*(?:error|err|e)\s*\)/,/if\s*\(!.*\)\s*\{?\s*(?:return|throw)\s/]};class AIConfidenceScorer{constructor(e={}){this.rootPath=e.rootPath||process.cwd(),this.hallDetector=e.hallucinationDetector||new HallucinationDetector({rootPath:this.rootPath})}async scan(e){const t=[];let s=new Map;try{const t=await this.hallDetector.scan(e),n=[...t.phantomAPIs||[],...t.deprecatedAPIs||[]];for(const e of n)s.has(e.file)||s.set(e.file,[]),s.get(e.file).push(e)}catch(e){}for(const n of e){let e;try{if(fs.statSync(n).size>524288)continue;e=fs.readFileSync(n,"utf-8").replace(/\r\n/g,"\n")}catch(e){continue}const i=path.relative(this.rootPath,n);if(this._isSkippable(i))continue;const c=s.get(i)||[],r=this._extractFunctions(e,i,c);t.push(...r)}t.sort((e,t)=>t.confidence-e.confidence);const n=t.filter(e=>e.confidence>=70),i=t.filter(e=>e.confidence>=40&&e.confidence<70),c=t.filter(e=>e.riskScore>=70),r=t.filter(e=>e.riskScore>=40&&e.riskScore<70);return{functions:t,highConfidence:n,mediumConfidence:i,totalFunctions:t.length,aiLikelyCount:n.length,aiPossibleCount:i.length,aiPercentage:t.length>0?Math.round(n.length/t.length*100):0,highRisk:c,mediumRisk:r,highRiskCount:c.length,mediumRiskCount:r.length}}_extractFunctions(e,t,s=[]){const n=e.split("\n"),i=[];for(let e=0;e<n.length;e++){const c=this._detectFunction(n,e);if(!c)continue;const r=this._findFunctionEnd(n,e),o=n.slice(e,r+1).join("\n"),a=n.slice(e,r+1),l=e+1,h=r+1,u=this._scoreFunction(c.name,o,a,n,e),m=this._scoreRisk(o,a,s,l,h);i.push({file:t,name:c.name,line:e+1,lineCount:r-e+1,confidence:u.score,signals:u.reasons,riskScore:m.score,riskSignals:m.reasons,riskSeverity:m.score>=70?"critical":m.score>=40?"high":m.score>=15?"medium":"low",testCoverage:this._hasTests(c.name,t)?"found":"none",category:"ai_confidence",severity:u.score>=80?"high":u.score>=50?"medium":"low"}),e=r}return i}_scoreRisk(e,t,s,n,i){let c=0;const r=[];RISK_SIGNALS.evalUsage.test(e)&&(c+=40,r.push("eval() usage — arbitrary code execution risk"));const o=s.filter(e=>e.line>=n&&e.line<=i);o.length>0&&(c+=Math.min(30,15*o.length),r.push(`${o.length} hallucinated/deprecated API usage(s): ${o.map(e=>e.name).slice(0,3).join(", ")}`));const a=this._maxBraceDepth(t);a>3&&(c+=Math.min(20,8*(a-3)),r.push(`Deep nesting — ${a} levels (>3 is risky)`));const l=this._cyclomaticComplexity(e);return l>10&&(c+=Math.min(15,Math.round(1.5*(l-10))),r.push(`High cyclomatic complexity (${l})`)),RISK_SIGNALS.hardcodedSecret.test(e)&&(c+=25,r.push("Hardcoded secret/credential detected")),RISK_SIGNALS.commandInjection.test(e)&&(c+=25,r.push("Command injection risk — exec/spawn with unsanitized string concatenation")),RISK_SIGNALS.sqlInjection.test(e)&&(c+=25,r.push("SQL injection risk — query built via string concatenation/interpolation")),c=Math.min(100,c),{score:c,reasons:r}}_maxBraceDepth(e){let t=0,s=0;for(const n of e)for(const e of n)"{"===e?(t++,t>s&&(s=t)):"}"===e&&(t=Math.max(0,t-1));return Math.max(0,s-1)}_cyclomaticComplexity(e){return 1+(e.match(/\b(if|else if|for|while|case|catch)\b|\?\s*[^:]+:|&&|\|\|/g)||[]).length}_scoreFunction(e,t,s,n,i){let c=0;const r=[],o=AI_SIGNALS.genericNames.filter(e=>{const s=new RegExp(`\\b${e}\\b`,"g");return(t.match(s)||[]).length>=2}).length;o>=3?(c+=15,r.push(`${o} generic variable names (data, result, response...)`)):o>=1&&(c+=8,r.push(`${o} generic variable name(s)`));const a=AI_SIGNALS.aiCommentPatterns.filter(e=>e.test(t)).length;if(a>=2?(c+=20,r.push(`${a} AI-style comments (TODO: implement, handle error, etc.)`)):a>=1&&(c+=10,r.push("AI-style comment detected")),i>0){const e=n.slice(Math.max(0,i-10),i).join("\n");/\/\*\*[\s\S]*@param[\s\S]*@returns?[\s\S]*@(?:throws|example)/s.test(e)&&(c+=10,r.push("Exhaustive JSDoc (params + returns + throws/example)"))}(t.match(/try\s*\{/g)||[]).length>=2&&(c+=10,r.push("Multiple try/catch blocks (AI over-handles errors)"));const l=s.length;return l>=50?(c+=15,r.push(`${l} lines — large self-contained function`)):l>=25&&(c+=8,r.push(`${l} lines`)),0===n.slice(0,i).concat(n.slice(i+l)).filter(t=>t.includes(e)).length&&l>10&&(c+=10,r.push("Isolated — no internal references")),/catch\s*\([^)]*\)\s*\{[^}]*console\.(log|error)\s*\(/.test(t)&&(c+=8,r.push("console.log/error in catch block (AI default pattern)")),/=>\s*\{[\s\n\r]*const\s*\{/.test(t)&&(c+=5,r.push("Immediate destructuring after arrow (common AI pattern)")),(t.match(/`[^`]*\$\{/g)||[]).length>=4&&(c+=5,r.push("Heavy template literal usage")),c=Math.min(100,c),{score:c,reasons:r}}_detectFunction(e,t){const s=e[t].trim();let n=s.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=s.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n&&(s.includes("=>")||s.includes("function"))?{name:n[1]}:(n=s.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=s.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=s.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=s.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=s.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),n?{name:n[1]}:null))))))}_findFunctionEnd(e,t){const s=(e[t]||"").trim();if(/^def\s/.test(s)){let s=1;for(let n=t+1;n<e.length&&n<t+300;n++){const t=e[n].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)return n}return Math.min(t+20,e.length-1)}let n=0,i=!1;for(let s=t;s<e.length&&s<t+300;s++){for(const t of e[s])"{"===t&&(n++,i=!0),"}"===t&&n--;if(i&&n<=0)return s}return Math.min(t+20,e.length-1)}_hasTests(e,t){return[t.replace(/\.(js|ts)$/,".test.$1"),t.replace(/\.(js|ts)$/,".spec.$1"),t.replace(/src\//,"test/"),t.replace(/src\//,"__tests__/")].some(e=>{try{return fs.existsSync(path.resolve(this.rootPath,e))}catch(e){return!1}})}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/node_modules/i,/\.min\./i].some(t=>t.test(e))}}module.exports=AIConfidenceScorer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const fs=require("fs"),path=require("path"),EventEmitter=require("events"),astAnalyzer=require("./ast-analyzer"),taintTracker=require("./taint-tracker"),cfgAnalyzer=require("./cfg-analyzer"),{getCodeScannerPatterns:getCodeScannerPatterns}=require("../crucible/rule-loader.js");class CodeScanner extends EventEmitter{constructor(e={}){super();const t=new Set(["rootPath","ignorePatterns","maxFileSize","totalScanTimeout","perFileTimeout","complexityThreshold","nestingThreshold"]),s={};for(const n of Object.keys(e))t.has(n)&&(s[n]=e[n]);this.config={rootPath:s.rootPath||process.cwd(),ignorePatterns:s.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],maxFileSize:s.maxFileSize||5242880,totalScanTimeout:s.totalScanTimeout||3e5,perFileTimeout:s.perFileTimeout||1e4,complexityThreshold:s.complexityThreshold||15,nestingThreshold:s.nestingThreshold||4},this._scanStartTime=null,this.safePatterns=[/keyEnv\s*[:=]/i,/envVar\s*[:=]/i,/process\.env\[/,/process\.env\./,/model\s*[:=]\s*['"][^'"]+['"]/i,/version\s*[:=]\s*['"][^'"]+['"]/i,/claude-|gpt-|gemini-|llama-|mistral-|deepseek-|grok-|sonar-/i,/endpoint\s*[:=]/i,/https?:\/\//,/_API_KEY['"]/,/API_KEY['"]\s*\)/,/errors?\.\w+\s*=/i,/Error\s*[:=]/i,/required|invalid|must be|cannot be/i],this.testFilePatterns=[/\.test\.(js|ts|jsx|tsx)$/,/\.spec\.(js|ts|jsx|tsx)$/,/__tests__\//,/\.mock\.(js|ts)$/,/test-.*\.js$/],this.securityPatterns=getCodeScannerPatterns(),this.qualityPatterns=[{id:"QUAL001",name:"Console.log in Production",pattern:/console\.(log|debug|info)\(/,severity:"info",message:"console.log found - consider using proper logging",skipInTests:!0,skipInDirs:["test","scripts","tools","dev"]},{id:"QUAL002",name:"TODO Comment",pattern:/\/\/\s*TODO|\/\*\s*TODO|\*\s*TODO|#\s*TODO/i,severity:"info",message:"TODO comment found - track in issue system"},{id:"QUAL003",name:"FIXME Comment",pattern:/\/\/\s*FIXME|\/\*\s*FIXME|\*\s*FIXME|#\s*FIXME/i,severity:"warning",message:"FIXME comment found - needs attention"},{id:"QUAL004",name:"Empty Catch Block",pattern:/catch\s*\([^)]*\)\s*\{\s*\}/,severity:"warning",message:"Empty catch block swallows errors"},{id:"QUAL005",name:"Magic Number",pattern:/(?<![0-9a-zA-Z_])(?!(?:1000|2000|3000|3001|4000|5000|8000|8080|9000|80|443|24|60|1024|2048|4096)\b)\d{5,}(?![0-9])/,severity:"info",message:"Magic number detected - consider using named constant",skipInTests:!0},{id:"QUAL006",name:"Callback Hell",pattern:/\)\s*=>\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/,severity:"warning",message:"Deeply nested callbacks - consider async/await"}],this.performancePatterns=[{id:"PERF001",name:"Sync FS in Async",pattern:/fs\.(readFileSync|writeFileSync|readdirSync|statSync)/,severity:"warning",message:"Synchronous file operation - may block event loop"},{id:"PERF002",name:"Missing Await",pattern:/async\s+function[^{]*\{[^}]*(?<!await\s)new\s+Promise/,severity:"warning",message:"Promise created in async function without await"},{id:"PERF003",name:"Loop Database Query",pattern:/for\s*\([^)]*\)\s*\{[^}]*(query|find|select|update)/i,severity:"error",message:"Possible N+1 query pattern - consider batch operation"},{id:"PERF004",name:"Large Array in Memory",pattern:/new\s+Array\s*\(\s*\d{6,}\s*\)/,severity:"warning",message:"Large array allocation - consider streaming"}],this.aiPatterns=[{id:"AI001",name:"Potentially Hallucinated Import",pattern:/require\(['"](?!\.)[^'"]+['"]\)/,severity:"info",message:"External dependency - verify it exists in package.json",check:(e,t,s)=>this._checkDependencyExists(e,s)},{id:"AI002",name:"Unused Import",pattern:/const\s+(\w+)\s*=\s*require\(['"][^'"]+['"]\)/,severity:"info",message:"Import may be unused - verify usage",check:(e,t)=>this._checkImportUsage(e,t)},{id:"AI003",name:"Over-Engineered Pattern",pattern:/class\s+\w+Factory\s*\{|AbstractFactory|Singleton\.getInstance/,severity:"info",message:"Complex pattern detected - ensure justified by requirements"}]}async scanAll(){console.log("[CODE-SCANNER] Starting full codebase scan...");const e=Date.now();this._scanStartTime=e;const t={filesScanned:0,filesSkipped:0,issues:[],summary:{},duration:0};try{const s=await this._discoverFiles(this.config.rootPath);console.log(`[CODE-SCANNER] Found ${s.length} files to scan`),this._exportTaintMap=this._buildExportTaintMap(s);for(const n of s){if(Date.now()-e>this.config.totalScanTimeout){console.warn(`[CODE-SCANNER] Total scan timeout (${Math.round(this.config.totalScanTimeout/1e3)}s) reached — stopping scan`),t.issues.push({file:"scan",category:"info",severity:"warning",id:"TIMEOUT001",message:`Scan stopped after ${Math.round(this.config.totalScanTimeout/1e3)}s timeout — ${s.length-t.filesScanned} files not scanned`});break}const i=Date.now(),r=await this.scanFile(n),a=Date.now()-i;a>this.config.perFileTimeout&&console.warn(`[CODE-SCANNER] File ${n} took ${a}ms (>${this.config.perFileTimeout}ms) — results included but flagged`),t.filesScanned++,t.issues.push(...r)}return this._exportTaintMap=null,t.duration=Date.now()-e,t.summary=this._summarizeIssues(t.issues),console.log(`[CODE-SCANNER] Scan complete: ${t.filesScanned} files, ${t.issues.length} issues`),t}catch(e){return console.error("[CODE-SCANNER] Scan failed"),t.error="Scan failed",t}}async scanFiles(e){const t=Date.now();this._scanStartTime=t;const s={filesScanned:0,filesSkipped:0,issues:[],summary:{}};this._exportTaintMap=this._buildExportTaintMap(e);for(const n of e){if(Date.now()-t>this.config.totalScanTimeout){console.warn(`[CODE-SCANNER] Total scan timeout (${Math.round(this.config.totalScanTimeout/1e3)}s) reached — stopping scan`),s.issues.push({file:"scan",category:"info",severity:"warning",id:"TIMEOUT001",message:`Scan stopped after ${Math.round(this.config.totalScanTimeout/1e3)}s timeout — ${e.length-s.filesScanned} files not scanned`});break}const i=Date.now(),r=await this.scanFile(n),a=Date.now()-i;a>this.config.perFileTimeout&&(console.warn(`[CODE-SCANNER] WARNING: ${n} took ${a}ms (>${this.config.perFileTimeout}ms) — skipping slow file`),s.filesSkipped++,s.issues.push({file:n,category:"performance",severity:"warning",id:"TIMEOUT002",message:`File scan took ${a}ms — exceeded ${this.config.perFileTimeout}ms per-file limit`})),s.filesScanned++,s.issues.push(...r)}return this._exportTaintMap=null,s.summary=this._summarizeIssues(s.issues),s}async scanFile(e){const t=[];try{const s=fs.statSync(e);if(s.isSymbolicLink&&s.isSymbolicLink())return[];if(0===s.size)return[];if(s.size>this.config.maxFileSize)return[{file:e,category:"quality",severity:"info",id:"FILE001",message:`File too large (${(s.size/1048576).toFixed(1)}MB > ${(this.config.maxFileSize/1048576).toFixed(0)}MB limit) - skipped`}];let n;try{n=fs.readFileSync(e)}catch(e){return[]}const i=Math.min(n.length,512);for(let e=0;e<i;e++)if(0===n[e])return[];const r=n.toString("utf8"),a=r.split("\n"),o=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".py",".pyw",".go",".rs",".java",".kt",".cs",".php",".rb"].includes(o)&&(t.push(...this._runPatternScan(e,r,a,this.securityPatterns,"security")),t.push(...this._runPatternScan(e,r,a,this.qualityPatterns,"quality")),t.push(...this._runPatternScan(e,r,a,this.performancePatterns,"performance")),t.push(...this._runPatternScan(e,r,a,this.aiPatterns,"ai")),t.push(...this._analyzeComplexity(e,r,a)),t.push(...this._analyzeNesting(e,r,a)),t.push(...this._runAstScan(e,r,t)),t.push(...this._runTaintScan(e,r,t)),t.push(...this._runCfgScan(e,r,t))),".json"===o&&t.push(...this._scanJson(e,r));const c=this._applyThubanIgnore(a,t);for(const e of c)this.emit("issueFound",e);return c}catch(t){return[{file:e,category:"error",severity:"error",id:"SCAN001",message:"Failed to scan file: "+("ENOENT"===t.code?"File not found":"EACCES"===t.code?"Permission denied":"EISDIR"===t.code?"Path is a directory":"Scan error")}]}}_applyThubanIgnore(e,t){if(!t.length)return t;const s=new Map;for(let t=0;t<e.length;t++){const n=e[t].match(/thuban-ignore(?:\s+([A-Za-z0-9_,\s]+))?/);if(!n)continue;const i=n[1],r=[t+1,t+2];for(const e of r){if(!i||!i.trim()){s.set(e,!0);continue}const t=i.split(",").map(e=>e.trim()).filter(Boolean),n=s.get(e);if(!0===n)continue;const r=n instanceof Set?n:new Set;for(const e of t)r.add(e);s.set(e,r)}}return 0===s.size?t:t.filter(e=>{if(null==e.line)return!0;const t=s.get(e.line);return!t||!0!==t&&!t.has(e.id)})}getRules(){return{security:this.securityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),quality:this.qualityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),performance:this.performancePatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),ai:this.aiPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity}))}}async _discoverFiles(e,t=[],s=0){if(s>100)return t;try{const n=fs.readdirSync(e,{withFileTypes:!0});for(const i of n){const n=path.join(e,i.name),r=path.relative(this.config.rootPath,n);if(!this._shouldIgnore(r))if(i.isDirectory())await this._discoverFiles(n,t,s+1);else if(i.isFile()){const e=path.extname(i.name).toLowerCase();[".js",".ts",".jsx",".tsx",".json",".py",".pyw",".go",".rs",".java",".kt",".cs",".php",".rb"].includes(e)&&t.push(n)}}}catch(e){}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),s=["node_modules",".git","dist",".vite"];for(const e of s)if(t.includes("/"+e+"/")||t.startsWith(e+"/")||t===e||t.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const s=e.replace(/\\/g,"/"),n=s.split("/")[0].replace(/\*/g,"");if(n&&(t===n||t.startsWith(n+"/")))return!0;let i=s.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+i).test(t))return!0}return!1}_runPatternScan(e,t,s,n,i){const r=[],a=path.basename(e),o=this.testFilePatterns.some(e=>e.test(a))||this.testFilePatterns.some(t=>t.test(e)&&!t.source.includes("\\.js$")),c=path.relative(this.config.rootPath,e).replace(/\\/g,"/");for(const a of n){if(a.skipInTests&&o)continue;if(a.skipInDirs&&a.skipInDirs.some(e=>c.startsWith(e+"/")||c.includes("/"+e+"/")))continue;if(a.excludeExtensions){const t=path.extname(e).toLowerCase();if(a.excludeExtensions.includes(t))continue}if(a.context&&!a.lineContext&&!a.context.test(t))continue;let n=0;for(const o of s){n++;const s=o.match(a.pattern);if(s){if(a.skipIfSafe&&this.safePatterns.some(e=>e.test(o)))continue;if(a.check&&!a.check(s,t,e))continue;if(("sql_injection"===a.type||"xss"===a.type)&&/\b(?:parseInt|parseFloat|Number|int|float|str|strconv\.(?:Atoi|Itoa|ParseInt|ParseFloat|ParseBool|FormatInt)|\.to_i|\.to_f|\.to_s|CGI\.escapeHTML|ERB::Util\.html_escape|html\.EscapeString|url\.QueryEscape|encodeURIComponent|encodeURI|htmlspecialchars|htmlentities)\b/.test(o))continue;r.push({file:e,line:n,column:s.index,category:i,type:a.type||i,severity:a.severity,id:a.id,name:a.name,message:a.message,code:o.trim().substring(0,100)})}}}return r}_runAstScan(e,t,s){let n;try{n=astAnalyzer.analyze(e,t)}catch(e){return[]}if(!n||0===n.length)return[];const i=new Set(s.map(e=>`${e.line}:${e.type}`));return n.filter(e=>!i.has(`${e.line}:${e.type}`))}_buildExportTaintMap(e){try{return taintTracker.buildExportTaintMap(e)}catch(e){return new Map}}_runTaintScan(e,t,s){let n;try{const s=path.extname(e).toLowerCase();if(new Set([".js",".jsx",".ts",".tsx",".mjs",".cjs"]).has(s)){let s;if(this._exportTaintMap){const n=taintTracker._internal.parseJS(t);n&&(s=taintTracker.buildImportSeeds(e,n,this._exportTaintMap,fs.existsSync))}n=taintTracker.analyzeJSTaint(e,t,s&&s.size?s:void 0)}else n=taintTracker.analyze(e,t)}catch(e){return[]}return n&&0!==n.length?n:[]}_runCfgScan(e,t,s){let n;try{n=cfgAnalyzer.analyze(e,t)}catch(e){return[]}if(!n||0===n.length)return[];const i=new Set(s.map(e=>`${e.line}:${e.type}`));return n.filter(e=>!i.has(`${e.line}:${e.type}`))}_analyzeComplexity(e,t,s){const n=[],i=t.match(/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+|=>\s*\{|(?:^|\n)\s*func\s+(?:\([^)]*\)\s+)?\w+\s*\(|(?:^|\n)\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+|(?:^|\n)\s*def\s+(?:self\.)?\w+|(?:^|\n)\s*(?:public|private|protected|internal)?\s*(?:fun|suspend\s+fun)\s+\w+/gm)||[],r=t.match(/\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g)||[],a=i.length>0?r.length/i.length:0;return a>this.config.complexityThreshold&&n.push({file:e,category:"quality",severity:"warning",id:"CMPLX001",name:"High Complexity",message:`Average cyclomatic complexity is ${a.toFixed(1)} (threshold: ${this.config.complexityThreshold})`}),s.length>500&&n.push({file:e,category:"quality",severity:"info",id:"CMPLX002",name:"Long File",message:`File has ${s.length} lines - consider splitting`}),n}_analyzeNesting(e,t,s){const n=[],i=path.extname(e).toLowerCase();let r=0,a=0,o=0;if(".rb"===i){let e=0;for(const t of s){e++;const s=t.replace(/#.*$/,"").trim();if(!s)continue;const n=(s.match(/\b(?:def|do|if|unless|while|until|for|begin|case|class|module)\b/g)||[]).length;a+=n-(/\b(?:if|unless|while|until)\b/.test(s)&&!s.match(/^\s*(?:if|unless|while|until|elsif)\b/)&&1===n?1:0)-(s.match(/\bend\b/g)||[]).length,a<0&&(a=0),a>r&&(r=a,o=e)}}else if(".py"===i||".pyw"===i){let e=0;for(const n of s){if(e++,!n.trim()||n.trim().startsWith("#"))continue;const s=n.match(/^(\s*)/)[1].length,i=Math.floor(s/(t.includes("\t")?1:4));i>r&&(r=i,o=e)}}else{let e=0;for(const t of s)e++,a+=(t.match(/\{/g)||[]).length-(t.match(/\}/g)||[]).length,a>r&&(r=a,o=e)}return r>this.config.nestingThreshold&&n.push({file:e,line:o,category:"quality",severity:"warning",id:"NEST001",name:"Deep Nesting",message:`Max nesting depth is ${r} at line ${o} (threshold: ${this.config.nestingThreshold})`}),n}_scanJson(e,t){const s=[];try{JSON.parse(t)}catch(t){s.push({file:e,category:"quality",severity:"error",id:"JSON001",name:"Invalid JSON",message:`JSON parse error: ${t.message}`})}return/(password|secret|api[_-]?key|token)/i.test(t)&&s.push({file:e,category:"security",severity:"warning",id:"JSON002",name:"Sensitive Data in JSON",message:"Possible sensitive data in JSON file - verify not committed"}),s}_checkDependencyExists(e,t){const s=e[0].match(/require\(['"]([^'"]+)['"]\)/);if(!s)return!0;const n=s[1];if(n.startsWith("."))return!1;if(["fs","path","os","crypto","http","https","events","util","stream","child_process","url","querystring","buffer"].includes(n.split("/")[0]))return!1;try{let e=path.join(this.config.rootPath,"package.json");if(t.includes("server")){const t=path.join(this.config.rootPath,"server","package.json");fs.existsSync(t)&&(e=t)}const s=JSON.parse(fs.readFileSync(e,"utf8")),i={...s.dependencies||{},...s.devDependencies||{}};return!i[n.split("/")[0]]&&!i[n]}catch(e){return!1}}_checkImportUsage(e,t){const s=e[1];if(!s)return!1;const n=new RegExp("\\b"+s+"\\b","g");return(t.match(n)||[]).length<=1}_summarizeIssues(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,error:e.filter(e=>"error"===e.severity).length,warning:e.filter(e=>"warning"===e.severity).length,info:e.filter(e=>"info"===e.severity).length},byCategory:e.reduce((e,t)=>(e[t.category]=(e[t.category]||0)+1,e),{}),topFiles:this._getTopFiles(e,10)}}_getTopFiles(e,t){const s=e.reduce((e,t)=>(e[t.file]=(e[t.file]||0)+1,e),{});return Object.entries(s).sort((e,t)=>t[1]-e[1]).slice(0,t).map(([e,t])=>({file:path.relative(this.config.rootPath,e),count:t}))}}module.exports=CodeScanner;
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events"),astAnalyzer=require("./ast-analyzer"),taintTracker=require("./taint-tracker"),cfgAnalyzer=require("./cfg-analyzer"),{getCodeScannerPatterns:getCodeScannerPatterns}=require("../crucible/rule-loader.js");class CodeScanner extends EventEmitter{constructor(e={}){super();const t=new Set(["rootPath","ignorePatterns","maxFileSize","totalScanTimeout","perFileTimeout","complexityThreshold","nestingThreshold"]),s={};for(const n of Object.keys(e))t.has(n)&&(s[n]=e[n]);this.config={rootPath:s.rootPath||process.cwd(),ignorePatterns:s.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],maxFileSize:s.maxFileSize||5242880,totalScanTimeout:s.totalScanTimeout||3e5,perFileTimeout:s.perFileTimeout||1e4,complexityThreshold:s.complexityThreshold||15,nestingThreshold:s.nestingThreshold||4},this._scanStartTime=null,this.safePatterns=[/keyEnv\s*[:=]/i,/envVar\s*[:=]/i,/process\.env\[/,/process\.env\./,/model\s*[:=]\s*['"][^'"]+['"]/i,/version\s*[:=]\s*['"][^'"]+['"]/i,/claude-|gpt-|gemini-|llama-|mistral-|deepseek-|grok-|sonar-/i,/endpoint\s*[:=]/i,/https?:\/\//,/_API_KEY['"]/,/API_KEY['"]\s*\)/,/errors?\.\w+\s*=/i,/Error\s*[:=]/i,/required|invalid|must be|cannot be/i],this.testFilePatterns=[/\.test\.(js|ts|jsx|tsx)$/,/\.spec\.(js|ts|jsx|tsx)$/,/__tests__\//,/\.mock\.(js|ts)$/,/test-.*\.js$/],this.securityPatterns=getCodeScannerPatterns(),this.qualityPatterns=[{id:"QUAL001",name:"Console.log in Production",pattern:/console\.(log|debug|info)\(/,severity:"info",message:"console.log found - consider using proper logging",skipInTests:!0,skipInDirs:["test","scripts","tools","dev"]},{id:"QUAL002",name:"TODO Comment",pattern:/\/\/\s*TODO|\/\*\s*TODO|\*\s*TODO|#\s*TODO/i,severity:"info",message:"TODO comment found - track in issue system"},{id:"QUAL003",name:"FIXME Comment",pattern:/\/\/\s*FIXME|\/\*\s*FIXME|\*\s*FIXME|#\s*FIXME/i,severity:"warning",message:"FIXME comment found - needs attention"},{id:"QUAL004",name:"Empty Catch Block",pattern:/catch\s*\([^)]*\)\s*\{\s*\}/,severity:"warning",message:"Empty catch block swallows errors"},{id:"QUAL005",name:"Magic Number",pattern:/(?<![0-9a-zA-Z_])(?!(?:1000|2000|3000|3001|4000|5000|8000|8080|9000|80|443|24|60|1024|2048|4096)\b)\d{5,}(?![0-9])/,severity:"info",message:"Magic number detected - consider using named constant",skipInTests:!0},{id:"QUAL006",name:"Callback Hell",pattern:/\)\s*=>\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/,severity:"warning",message:"Deeply nested callbacks - consider async/await"}],this.performancePatterns=[{id:"PERF001",name:"Sync FS in Async",pattern:/fs\.(readFileSync|writeFileSync|readdirSync|statSync)/,severity:"warning",message:"Synchronous file operation - may block event loop"},{id:"PERF002",name:"Missing Await",pattern:/async\s+function[^{]*\{[^}]*(?<!await\s)new\s+Promise/,severity:"warning",message:"Promise created in async function without await"},{id:"PERF003",name:"Loop Database Query",pattern:/for\s*\([^)]*\)\s*\{[^}]*(query|find|select|update)/i,severity:"error",message:"Possible N+1 query pattern - consider batch operation"},{id:"PERF004",name:"Large Array in Memory",pattern:/new\s+Array\s*\(\s*\d{6,}\s*\)/,severity:"warning",message:"Large array allocation - consider streaming"}],this.aiPatterns=[{id:"AI001",name:"Potentially Hallucinated Import",pattern:/require\(['"](?!\.)[^'"]+['"]\)/,severity:"info",message:"External dependency - verify it exists in package.json",check:(e,t,s)=>this._checkDependencyExists(e,s)},{id:"AI002",name:"Unused Import",pattern:/const\s+(\w+)\s*=\s*require\(['"][^'"]+['"]\)/,severity:"info",message:"Import may be unused - verify usage",check:(e,t)=>this._checkImportUsage(e,t)},{id:"AI003",name:"Over-Engineered Pattern",pattern:/class\s+\w+Factory\s*\{|AbstractFactory|Singleton\.getInstance/,severity:"info",message:"Complex pattern detected - ensure justified by requirements"}]}async scanAll(){console.log("[CODE-SCANNER] Starting full codebase scan...");const e=Date.now();this._scanStartTime=e;const t={filesScanned:0,filesSkipped:0,issues:[],summary:{},duration:0};try{const s=await this._discoverFiles(this.config.rootPath);console.log(`[CODE-SCANNER] Found ${s.length} files to scan`),this._exportTaintMap=this._buildExportTaintMap(s);for(const n of s){if(Date.now()-e>this.config.totalScanTimeout){console.warn(`[CODE-SCANNER] Total scan timeout (${Math.round(this.config.totalScanTimeout/1e3)}s) reached — stopping scan`),t.issues.push({file:"scan",category:"info",severity:"warning",id:"TIMEOUT001",message:`Scan stopped after ${Math.round(this.config.totalScanTimeout/1e3)}s timeout — ${s.length-t.filesScanned} files not scanned`});break}const i=Date.now(),r=await this.scanFile(n),a=Date.now()-i;a>this.config.perFileTimeout&&console.warn(`[CODE-SCANNER] File ${n} took ${a}ms (>${this.config.perFileTimeout}ms) — results included but flagged`),t.filesScanned++,t.issues.push(...r)}return this._exportTaintMap=null,t.duration=Date.now()-e,t.summary=this._summarizeIssues(t.issues),console.log(`[CODE-SCANNER] Scan complete: ${t.filesScanned} files, ${t.issues.length} issues`),t}catch(e){return console.error("[CODE-SCANNER] Scan failed"),t.error="Scan failed",t}}async scanFiles(e){const t=Date.now();this._scanStartTime=t;const s={filesScanned:0,filesSkipped:0,issues:[],summary:{}};this._exportTaintMap=this._buildExportTaintMap(e);for(const n of e){if(Date.now()-t>this.config.totalScanTimeout){console.warn(`[CODE-SCANNER] Total scan timeout (${Math.round(this.config.totalScanTimeout/1e3)}s) reached — stopping scan`),s.issues.push({file:"scan",category:"info",severity:"warning",id:"TIMEOUT001",message:`Scan stopped after ${Math.round(this.config.totalScanTimeout/1e3)}s timeout — ${e.length-s.filesScanned} files not scanned`});break}const i=Date.now(),r=await this.scanFile(n),a=Date.now()-i;a>this.config.perFileTimeout&&(console.warn(`[CODE-SCANNER] WARNING: ${n} took ${a}ms (>${this.config.perFileTimeout}ms) — skipping slow file`),s.filesSkipped++,s.issues.push({file:n,category:"performance",severity:"warning",id:"TIMEOUT002",message:`File scan took ${a}ms — exceeded ${this.config.perFileTimeout}ms per-file limit`})),s.filesScanned++,s.issues.push(...r)}return this._exportTaintMap=null,s.summary=this._summarizeIssues(s.issues),s}async scanFile(e){const t=[];try{const s=fs.statSync(e);if(s.isSymbolicLink&&s.isSymbolicLink())return[];if(0===s.size)return[];if(s.size>this.config.maxFileSize)return[{file:e,category:"quality",severity:"info",id:"FILE001",message:`File too large (${(s.size/1048576).toFixed(1)}MB > ${(this.config.maxFileSize/1048576).toFixed(0)}MB limit) - skipped`}];let n;try{n=fs.readFileSync(e)}catch(e){return[]}const i=Math.min(n.length,512);for(let e=0;e<i;e++)if(0===n[e])return[];const r=n.toString("utf8").replace(/\r\n/g,"\n"),a=r.split("\n"),o=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".vue",".svelte",".astro",".py",".pyw",".pyi",".pyx",".go",".rs",".java",".jsp",".kt",".kts",".cs",".cshtml",".razor",".vb",".fs",".fsx",".fsi",".php",".phtml",".blade.php",".rb",".erb",".rake",".gemspec",".swift",".m",".mm",".c",".h",".cpp",".cc",".cxx",".hpp",".hxx",".hh",".ino",".scala",".sc",".dart",".lua",".pl",".pm",".t",".r",".R",".Rmd",".groovy",".gvy",".gradle",".ex",".exs",".heex",".erl",".hrl",".hs",".lhs",".clj",".cljs",".cljc",".edn",".ml",".mli",".re",".rei",".nim",".nims",".zig",".cr",".jl",".d",".adb",".ads",".f90",".f95",".f03",".f08",".f",".for",".cob",".cbl",".cpy",".pas",".pp",".dpr",".cls",".trigger",".abap",".ps1",".psm1",".psd1",".sh",".bash",".zsh",".fish",".bat",".cmd",".sol",".move",".vy",".sql",".psql",".plsql",".graphql",".gql",".proto",".thrift",".html",".htm",".xhtml",".ejs",".hbs",".handlebars",".mustache",".pug",".jade",".haml",".slim",".twig",".njk",".liquid",".jinja",".jinja2",".css",".scss",".sass",".less",".styl",".xml",".xsl",".xslt",".xsd",".wsdl",".svg",".plist",".csproj",".sln",".props",".json",".jsonc",".json5",".yaml",".yml",".toml",".ini",".cfg",".conf",".env",".properties",".tf",".hcl",".dockerfile",".bicep",".pp",".md",".mdx",".rst",".adoc",".tex",".ipynb",".cmake",".as",".coffee",".ls",".elm",".purs",".v",".vv",".mojo",".tcl",".scm",".ss",".rkt",".lisp",".lsp",".cl",".pro",".P",".st",".vhd",".vhdl",".sv",".svh"].includes(o)&&(t.push(...this._runPatternScan(e,r,a,this.securityPatterns,"security")),t.push(...this._runPatternScan(e,r,a,this.qualityPatterns,"quality")),t.push(...this._runPatternScan(e,r,a,this.performancePatterns,"performance")),t.push(...this._runPatternScan(e,r,a,this.aiPatterns,"ai")),t.push(...this._analyzeComplexity(e,r,a)),t.push(...this._analyzeNesting(e,r,a)),t.push(...this._runAstScan(e,r,t)),t.push(...this._runTaintScan(e,r,t)),t.push(...this._runCfgScan(e,r,t))),".json"===o&&t.push(...this._scanJson(e,r));const c=this._applyThubanIgnore(a,t);for(const e of c)this.emit("issueFound",e);return c}catch(t){return[{file:e,category:"error",severity:"error",id:"SCAN001",message:"Failed to scan file: "+("ENOENT"===t.code?"File not found":"EACCES"===t.code?"Permission denied":"EISDIR"===t.code?"Path is a directory":"Scan error")}]}}_applyThubanIgnore(e,t){if(!t.length)return t;const s=new Map;for(let t=0;t<e.length;t++){const n=e[t].match(/thuban-ignore(?:\s+([A-Za-z0-9_,\s]+))?/);if(!n)continue;const i=n[1],r=[t+1,t+2];for(const e of r){if(!i||!i.trim()){s.set(e,!0);continue}const t=i.split(",").map(e=>e.trim()).filter(Boolean),n=s.get(e);if(!0===n)continue;const r=n instanceof Set?n:new Set;for(const e of t)r.add(e);s.set(e,r)}}return 0===s.size?t:t.filter(e=>{if(null==e.line)return!0;const t=s.get(e.line);return!t||!0!==t&&!t.has(e.id)})}getRules(){return{security:this.securityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),quality:this.qualityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),performance:this.performancePatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),ai:this.aiPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity}))}}async _discoverFiles(e,t=[],s=0){if(s>100)return t;try{const n=fs.readdirSync(e,{withFileTypes:!0});for(const i of n){const n=path.join(e,i.name),r=path.relative(this.config.rootPath,n);if(!this._shouldIgnore(r))if(i.isDirectory())await this._discoverFiles(n,t,s+1);else if(i.isFile()){const e=path.extname(i.name).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".vue",".svelte",".astro",".json",".jsonc",".json5",".py",".pyw",".pyi",".pyx",".go",".rs",".java",".jsp",".kt",".kts",".cs",".cshtml",".razor",".vb",".fs",".fsx",".fsi",".php",".phtml",".rb",".erb",".rake",".gemspec",".swift",".m",".mm",".c",".h",".cpp",".cc",".cxx",".hpp",".hxx",".hh",".ino",".scala",".sc",".dart",".lua",".pl",".pm",".t",".r",".R",".Rmd",".groovy",".gvy",".gradle",".ex",".exs",".heex",".erl",".hrl",".hs",".lhs",".clj",".cljs",".cljc",".edn",".ml",".mli",".re",".rei",".nim",".nims",".zig",".cr",".jl",".d",".adb",".ads",".f90",".f95",".f03",".f08",".f",".for",".cob",".cbl",".cpy",".pas",".pp",".dpr",".cls",".trigger",".abap",".ps1",".psm1",".psd1",".sh",".bash",".zsh",".fish",".bat",".cmd",".sol",".move",".vy",".sql",".psql",".plsql",".graphql",".gql",".proto",".thrift",".html",".htm",".xhtml",".ejs",".hbs",".handlebars",".mustache",".pug",".jade",".haml",".slim",".twig",".njk",".liquid",".jinja",".jinja2",".css",".scss",".sass",".less",".styl",".xml",".xsl",".xslt",".xsd",".wsdl",".svg",".plist",".csproj",".sln",".props",".yaml",".yml",".toml",".ini",".cfg",".conf",".env",".properties",".tf",".hcl",".dockerfile",".bicep",".md",".mdx",".rst",".adoc",".tex",".ipynb",".cmake",".as",".coffee",".ls",".elm",".purs",".v",".vv",".mojo",".tcl",".scm",".ss",".rkt",".lisp",".lsp",".cl",".pro",".P",".st",".vhd",".vhdl",".sv",".svh"].includes(e)&&t.push(n)}}}catch(e){}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),s=["node_modules",".git","dist",".vite"];for(const e of s)if(t.includes("/"+e+"/")||t.startsWith(e+"/")||t===e||t.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const s=e.replace(/\\/g,"/"),n=s.split("/")[0].replace(/\*/g,"");if(n&&(t===n||t.startsWith(n+"/")))return!0;let i=s.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+i).test(t))return!0}return!1}_runPatternScan(e,t,s,n,i){const r=[],a=path.basename(e),o=this.testFilePatterns.some(e=>e.test(a))||this.testFilePatterns.some(t=>t.test(e)&&!t.source.includes("\\.js$")),c=path.relative(this.config.rootPath,e).replace(/\\/g,"/");for(const a of n){if(a.skipInTests&&o)continue;if(a.skipInDirs&&a.skipInDirs.some(e=>c.startsWith(e+"/")||c.includes("/"+e+"/")))continue;if(a.excludeExtensions){const t=path.extname(e).toLowerCase();if(a.excludeExtensions.includes(t))continue}if(a.context&&!a.lineContext&&!a.context.test(t))continue;let n=0;for(const o of s){n++;const s=o.match(a.pattern);if(s){if(a.skipIfSafe&&this.safePatterns.some(e=>e.test(o)))continue;if(a.check&&!a.check(s,t,e))continue;if(("sql_injection"===a.type||"xss"===a.type)&&/\b(?:parseInt|parseFloat|Number|int|float|str|strconv\.(?:Atoi|Itoa|ParseInt|ParseFloat|ParseBool|FormatInt)|\.to_i|\.to_f|\.to_s|CGI\.escapeHTML|ERB::Util\.html_escape|html\.EscapeString|url\.QueryEscape|encodeURIComponent|encodeURI|htmlspecialchars|htmlentities)\b/.test(o))continue;r.push({file:e,line:n,column:s.index,category:i,type:a.type||i,severity:a.severity,id:a.id,name:a.name,message:a.message,code:o.trim().substring(0,100),...a.owasp&&{owasp:a.owasp},...a.cwe&&{cwe:a.cwe}})}}}return r}_runAstScan(e,t,s){let n;try{n=astAnalyzer.analyze(e,t)}catch(e){return[]}if(!n||0===n.length)return[];const i=new Set(s.map(e=>`${e.line}:${e.type}`));return n.filter(e=>!i.has(`${e.line}:${e.type}`))}_buildExportTaintMap(e){try{return taintTracker.buildExportTaintMap(e)}catch(e){return new Map}}_runTaintScan(e,t,s){let n;try{const s=path.extname(e).toLowerCase();if(new Set([".js",".jsx",".ts",".tsx",".mjs",".cjs"]).has(s)){let s;if(this._exportTaintMap){const n=taintTracker._internal.parseJS(t);n&&(s=taintTracker.buildImportSeeds(e,n,this._exportTaintMap,fs.existsSync))}n=taintTracker.analyzeJSTaint(e,t,s&&s.size?s:void 0)}else n=taintTracker.analyze(e,t)}catch(e){return[]}return n&&0!==n.length?n:[]}_runCfgScan(e,t,s){let n;try{n=cfgAnalyzer.analyze(e,t)}catch(e){return[]}if(!n||0===n.length)return[];const i=new Set(s.map(e=>`${e.line}:${e.type}`));return n.filter(e=>!i.has(`${e.line}:${e.type}`))}_analyzeComplexity(e,t,s){const n=[],i=t.match(/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+|=>\s*\{|(?:^|\n)\s*func\s+(?:\([^)]*\)\s+)?\w+\s*\(|(?:^|\n)\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+|(?:^|\n)\s*def\s+(?:self\.)?\w+|(?:^|\n)\s*(?:public|private|protected|internal)?\s*(?:fun|suspend\s+fun)\s+\w+/gm)||[],r=t.match(/\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g)||[],a=i.length>0?r.length/i.length:0;return a>this.config.complexityThreshold&&n.push({file:e,category:"quality",severity:"warning",id:"CMPLX001",name:"High Complexity",message:`Average cyclomatic complexity is ${a.toFixed(1)} (threshold: ${this.config.complexityThreshold})`}),s.length>500&&n.push({file:e,category:"quality",severity:"info",id:"CMPLX002",name:"Long File",message:`File has ${s.length} lines - consider splitting`}),n}_analyzeNesting(e,t,s){const n=[],i=path.extname(e).toLowerCase();let r=0,a=0,o=0;if(".rb"===i){let e=0;for(const t of s){e++;const s=t.replace(/#.*$/,"").trim();if(!s)continue;const n=(s.match(/\b(?:def|do|if|unless|while|until|for|begin|case|class|module)\b/g)||[]).length;a+=n-(/\b(?:if|unless|while|until)\b/.test(s)&&!s.match(/^\s*(?:if|unless|while|until|elsif)\b/)&&1===n?1:0)-(s.match(/\bend\b/g)||[]).length,a<0&&(a=0),a>r&&(r=a,o=e)}}else if(".py"===i||".pyw"===i){let e=0;for(const n of s){if(e++,!n.trim()||n.trim().startsWith("#"))continue;const s=n.match(/^(\s*)/)[1].length,i=Math.floor(s/(t.includes("\t")?1:4));i>r&&(r=i,o=e)}}else{let e=0;for(const t of s)e++,a+=(t.match(/\{/g)||[]).length-(t.match(/\}/g)||[]).length,a>r&&(r=a,o=e)}return r>this.config.nestingThreshold&&n.push({file:e,line:o,category:"quality",severity:"warning",id:"NEST001",name:"Deep Nesting",message:`Max nesting depth is ${r} at line ${o} (threshold: ${this.config.nestingThreshold})`}),n}_scanJson(e,t){const s=[];try{JSON.parse(t)}catch(t){s.push({file:e,category:"quality",severity:"error",id:"JSON001",name:"Invalid JSON",message:`JSON parse error: ${t.message}`})}return/(password|secret|api[_-]?key|token)/i.test(t)&&s.push({file:e,category:"security",severity:"warning",id:"JSON002",name:"Sensitive Data in JSON",message:"Possible sensitive data in JSON file - verify not committed"}),s}_checkDependencyExists(e,t){const s=e[0].match(/require\(['"]([^'"]+)['"]\)/);if(!s)return!0;const n=s[1];if(n.startsWith("."))return!1;const i=n.startsWith("node:")?n.slice(5).split("/")[0]:n.split("/")[0];if(["assert","async_hooks","buffer","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","domain","events","fs","http","http2","https","inspector","module","net","os","path","perf_hooks","process","punycode","querystring","readline","repl","stream","string_decoder","sys","timers","tls","trace_events","tty","url","util","v8","vm","wasi","worker_threads","zlib"].includes(i))return!1;const r=n.startsWith("@")?n.split("/").slice(0,2).join("/"):i;let a=path.dirname(t);const o=this.config.rootPath;for(;a.length>=o.length;){try{const e=path.join(a,"package.json");if(fs.existsSync(e)){const t=JSON.parse(fs.readFileSync(e,"utf8")),s={...t.dependencies||{},...t.devDependencies||{},...t.peerDependencies||{}};if(s[r]||s[n])return!1}}catch(e){}const e=path.dirname(a);if(e===a)break;a=e}return!0}_checkImportUsage(e,t){const s=e[1];if(!s)return!1;const n=new RegExp("\\b"+s+"\\b","g");return(t.match(n)||[]).length<=1}_summarizeIssues(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,error:e.filter(e=>"error"===e.severity).length,warning:e.filter(e=>"warning"===e.severity).length,info:e.filter(e=>"info"===e.severity).length},byCategory:e.reduce((e,t)=>(e[t.category]=(e[t.category]||0)+1,e),{}),topFiles:this._getTopFiles(e,10)}}_getTopFiles(e,t){const s=e.reduce((e,t)=>(e[t.file]=(e[t.file]||0)+1,e),{});return Object.entries(s).sort((e,t)=>t[1]-e[1]).slice(0,t).map(([e,t])=>({file:path.relative(this.config.rootPath,e),count:t}))}}module.exports=CodeScanner;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CodebasePassport{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}generate(e,t={}){const s=Date.now(),r=[],i={},n={},o=[],c=[];let a=0;const l={},u=new Set;for(const t of e){let s;try{if(fs.statSync(t).size>1048576)continue;s=fs.readFileSync(t,"utf-8")}catch(e){continue}const d=path.relative(this.rootPath,t),p=s.split("\n");a+=p.length;const h=path.extname(t);l[h]=(l[h]||0)+p.length,this._detectFrameworks(s,u);const m=this._extractDependencies(s,d),f=this._extractExports(s,d);i[d]=m,f.length>0&&(n[d]=f),this._isEntryPoint(d,s)&&o.push(d);const g=e.filter(e=>{try{const t=fs.readFileSync(e,"utf-8");return t.includes(d.replace(/\\/g,"/"))||t.includes(d.replace(/\//g,"\\"))}catch(e){return!1}}).length;g>5&&c.push({file:d,importedBy:g,risk:g>10?"critical":"high",note:`Changing this file affects ${g} other files. Proceed with caution.`});const y=this._detectPurpose(d,s);r.push({path:d,lines:p.length,purpose:y,imports:m.length,exports:f.length})}const d=this._findCriticalPaths(i),p=this._detectArchitecture(r);return{_version:"1.0.0",_generated:(new Date).toISOString(),_generator:"thuban",project:{name:path.basename(this.rootPath),rootPath:this.rootPath,totalFiles:e.length,totalLines:a,languages:Object.entries(l).sort((e,t)=>t[1]-e[1]).map(([e,t])=>({extension:e,lines:t,percentage:Math.round(t/a*100)})),frameworks:[...u],architecture:p,entryPoints:o},files:r.sort((e,t)=>t.imports-e.imports).slice(0,200),sacred:c.sort((e,t)=>t.importedBy-e.importedBy),dependencies:{totalChains:Object.keys(i).length,criticalPaths:d.slice(0,20),circularRisks:this._findCircularRisks(i)},onboarding:{estimatedReadTimeMinutes:Math.ceil(a/200),startHere:o.slice(0,5),keyFiles:c.slice(0,10).map(e=>e.file),doNotTouch:c.filter(e=>"critical"===e.risk).map(e=>e.file),quickSummary:this._generateQuickSummary(r,u,p)},generatedIn:Date.now()-s+"ms"}}save(e,t){const s=t||path.join(this.rootPath,".thuban-passport.json");return fs.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}_extractDependencies(e,t){const s=[],r=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const t of r){let r;for(;null!==(r=t.exec(e));)s.push(r[1])}return[...new Set(s)]}_extractExports(e,t){const s=[];e.match(/module\.exports\s*=\s*(\{[\s\S]*?\}|[a-zA-Z_$]\w*)/)&&s.push("module.exports");const r=e.match(/exports\.([a-zA-Z_$]\w*)\s*=/g);r&&r.forEach(e=>s.push(e.replace(/\s*=\s*$/,"")));const i=e.match(/export\s+(?:default\s+)?(?:const|let|var|function|class)\s+([a-zA-Z_$]\w*)/g);return i&&i.forEach(e=>s.push(e)),s}_isEntryPoint(e,t){return!!["index.","main.","app.","server.","cli.","bin/"].some(t=>e.includes(t))||!!t.includes("#!/usr/bin/env")||!(!t.includes("createServer")&&!t.includes(".listen("))}_detectPurpose(e,t){const s=e.toLowerCase();return/route/i.test(s)?"routing":/middleware/i.test(s)?"middleware":/model/i.test(s)?"data model":/controller/i.test(s)?"controller":/service/i.test(s)?"service":/util|helper|lib/i.test(s)?"utility":/config/i.test(s)?"configuration":/test|spec/i.test(s)?"test":/component/i.test(s)?"UI component":/hook/i.test(s)?"React hook":/store|redux|state/i.test(s)?"state management":/api/i.test(s)?"API layer":/db|database|migration/i.test(s)?"database":/auth/i.test(s)?"authentication":/cli|command/i.test(s)?"CLI":t.includes("app.get(")||t.includes("router.")?"routing":t.includes("mongoose.model")||t.includes("Schema")?"data model":t.includes("React")||t.includes("jsx")?"React component":"application logic"}_detectFrameworks(e,t){(e.includes("require('express'")||e.includes("from 'express'"))&&t.add("Express"),(e.includes("require('react'")||e.includes("from 'react'"))&&t.add("React"),(e.includes("require('next'")||e.includes("from 'next'"))&&t.add("Next.js"),(e.includes("require('mongoose'")||e.includes("from 'mongoose'"))&&t.add("Mongoose"),(e.includes("require('fastify'")||e.includes("from 'fastify'"))&&t.add("Fastify"),(e.includes("require('vue'")||e.includes("from 'vue'"))&&t.add("Vue"),(e.includes("require('stripe'")||e.includes("from 'stripe'"))&&t.add("Stripe"),e.includes("@angular")&&t.add("Angular")}_detectArchitecture(e){const t=e.map(e=>e.path.toLowerCase());return t.some(e=>e.includes("controller"))&&t.some(e=>e.includes("model"))?"MVC":t.some(e=>e.includes("service"))&&t.some(e=>e.includes("repository"))?"Service-Repository":t.some(e=>e.includes("component"))&&t.some(e=>e.includes("store"))?"Component-Store (SPA)":t.some(e=>e.includes("pages/"))&&t.some(e=>e.includes("api/"))?"Next.js (Pages Router)":t.some(e=>e.includes("app/"))&&t.some(e=>e.includes("route"))?"App Router":t.some(e=>e.includes("packages/"))?"Monorepo":"Flat"}_findCriticalPaths(e){return Object.entries(e).filter(([,e])=>e.length>3).map(([e,t])=>({file:e,dependsOn:t.length,topDeps:t.slice(0,5)})).sort((e,t)=>t.dependsOn-e.dependsOn).slice(0,20)}_findCircularRisks(e){const t=[];for(const[s,r]of Object.entries(e))for(const i of r){const r=this._resolveRelative(s,i);r&&e[r]&&e[r].some(e=>this._resolveRelative(r,e)===s)&&t.push({fileA:s,fileB:r,risk:"circular dependency"})}return t.slice(0,10)}_resolveRelative(e,t){if(t.startsWith(".")){const s=path.dirname(e);let r=path.join(s,t).replace(/\\/g,"/");return r.match(/\.\w+$/)||(r+=".js"),r}return null}_generateQuickSummary(e,t,s){return`${e.length}-file ${s} project using ${[...t].join(", ")||"vanilla"}. Start with entry points listed above.`}}module.exports=CodebasePassport;
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CodebasePassport{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}generate(e,t={}){const s=Date.now(),r=[],i={},n={},o=[],c=[];let a=0;const l={},u=new Set;for(const t of e){let s;try{if(fs.statSync(t).size>1048576)continue;s=fs.readFileSync(t,"utf-8").replace(/\r\n/g,"\n")}catch(e){continue}const d=path.relative(this.rootPath,t),p=s.split("\n");a+=p.length;const h=path.extname(t);l[h]=(l[h]||0)+p.length,this._detectFrameworks(s,u);const m=this._extractDependencies(s,d),f=this._extractExports(s,d);i[d]=m,f.length>0&&(n[d]=f),this._isEntryPoint(d,s)&&o.push(d);const g=e.filter(e=>{try{const t=fs.readFileSync(e,"utf-8").replace(/\r\n/g,"\n");return t.includes(d.replace(/\\/g,"/"))||t.includes(d.replace(/\//g,"\\"))}catch(e){return!1}}).length;g>5&&c.push({file:d,importedBy:g,risk:g>10?"critical":"high",note:`Changing this file affects ${g} other files. Proceed with caution.`});const y=this._detectPurpose(d,s);r.push({path:d,lines:p.length,purpose:y,imports:m.length,exports:f.length})}const d=this._findCriticalPaths(i),p=this._detectArchitecture(r);return{_version:"1.0.0",_generated:(new Date).toISOString(),_generator:"thuban",project:{name:path.basename(this.rootPath),rootPath:this.rootPath,totalFiles:e.length,totalLines:a,languages:Object.entries(l).sort((e,t)=>t[1]-e[1]).map(([e,t])=>({extension:e,lines:t,percentage:Math.round(t/a*100)})),frameworks:[...u],architecture:p,entryPoints:o},files:r.sort((e,t)=>t.imports-e.imports).slice(0,200),sacred:c.sort((e,t)=>t.importedBy-e.importedBy),dependencies:{totalChains:Object.keys(i).length,criticalPaths:d.slice(0,20),circularRisks:this._findCircularRisks(i)},onboarding:{estimatedReadTimeMinutes:Math.ceil(a/200),startHere:o.slice(0,5),keyFiles:c.slice(0,10).map(e=>e.file),doNotTouch:c.filter(e=>"critical"===e.risk).map(e=>e.file),quickSummary:this._generateQuickSummary(r,u,p)},generatedIn:Date.now()-s+"ms"}}save(e,t){const s=t||path.join(this.rootPath,".thuban-passport.json");return fs.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}_extractDependencies(e,t){const s=[],r=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const t of r){let r;for(;null!==(r=t.exec(e));)s.push(r[1])}return[...new Set(s)]}_extractExports(e,t){const s=[];e.match(/module\.exports\s*=\s*(\{[\s\S]*?\}|[a-zA-Z_$]\w*)/)&&s.push("module.exports");const r=e.match(/exports\.([a-zA-Z_$]\w*)\s*=/g);r&&r.forEach(e=>s.push(e.replace(/\s*=\s*$/,"")));const i=e.match(/export\s+(?:default\s+)?(?:const|let|var|function|class)\s+([a-zA-Z_$]\w*)/g);return i&&i.forEach(e=>s.push(e)),s}_isEntryPoint(e,t){return!!["index.","main.","app.","server.","cli.","bin/"].some(t=>e.includes(t))||!!t.includes("#!/usr/bin/env")||!(!t.includes("createServer")&&!t.includes(".listen("))}_detectPurpose(e,t){const s=e.toLowerCase();return/route/i.test(s)?"routing":/middleware/i.test(s)?"middleware":/model/i.test(s)?"data model":/controller/i.test(s)?"controller":/service/i.test(s)?"service":/util|helper|lib/i.test(s)?"utility":/config/i.test(s)?"configuration":/test|spec/i.test(s)?"test":/component/i.test(s)?"UI component":/hook/i.test(s)?"React hook":/store|redux|state/i.test(s)?"state management":/api/i.test(s)?"API layer":/db|database|migration/i.test(s)?"database":/auth/i.test(s)?"authentication":/cli|command/i.test(s)?"CLI":t.includes("app.get(")||t.includes("router.")?"routing":t.includes("mongoose.model")||t.includes("Schema")?"data model":t.includes("React")||t.includes("jsx")?"React component":"application logic"}_detectFrameworks(e,t){(e.includes("require('express'")||e.includes("from 'express'"))&&t.add("Express"),(e.includes("require('react'")||e.includes("from 'react'"))&&t.add("React"),(e.includes("require('next'")||e.includes("from 'next'"))&&t.add("Next.js"),(e.includes("require('mongoose'")||e.includes("from 'mongoose'"))&&t.add("Mongoose"),(e.includes("require('fastify'")||e.includes("from 'fastify'"))&&t.add("Fastify"),(e.includes("require('vue'")||e.includes("from 'vue'"))&&t.add("Vue"),(e.includes("require('stripe'")||e.includes("from 'stripe'"))&&t.add("Stripe"),e.includes("@angular")&&t.add("Angular")}_detectArchitecture(e){const t=e.map(e=>e.path.toLowerCase());return t.some(e=>e.includes("controller"))&&t.some(e=>e.includes("model"))?"MVC":t.some(e=>e.includes("service"))&&t.some(e=>e.includes("repository"))?"Service-Repository":t.some(e=>e.includes("component"))&&t.some(e=>e.includes("store"))?"Component-Store (SPA)":t.some(e=>e.includes("pages/"))&&t.some(e=>e.includes("api/"))?"Next.js (Pages Router)":t.some(e=>e.includes("app/"))&&t.some(e=>e.includes("route"))?"App Router":t.some(e=>e.includes("packages/"))?"Monorepo":"Flat"}_findCriticalPaths(e){return Object.entries(e).filter(([,e])=>e.length>3).map(([e,t])=>({file:e,dependsOn:t.length,topDeps:t.slice(0,5)})).sort((e,t)=>t.dependsOn-e.dependsOn).slice(0,20)}_findCircularRisks(e){const t=[];for(const[s,r]of Object.entries(e))for(const i of r){const r=this._resolveRelative(s,i);r&&e[r]&&e[r].some(e=>this._resolveRelative(r,e)===s)&&t.push({fileA:s,fileB:r,risk:"circular dependency"})}return t.slice(0,10)}_resolveRelative(e,t){if(t.startsWith(".")){const s=path.dirname(e);let r=path.join(s,t).replace(/\\/g,"/");return r.match(/\.\w+$/)||(r+=".js"),r}return null}_generateQuickSummary(e,t,s){return`${e.length}-file ${s} project using ${[...t].join(", ")||"vanilla"}. Start with entry points listed above.`}}module.exports=CodebasePassport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CopyPasteDriftDetector{constructor(t={}){this.rootPath=t.rootPath||process.cwd(),this.minLines=t.minLines||5,this.similarityThreshold=t.similarity||.7}scan(t){const e=[];for(const n of t){let t;try{if(fs.statSync(n).size>524288)continue;t=fs.readFileSync(n,"utf-8")}catch(t){continue}const s=path.relative(this.rootPath,n);if(this._isSkippable(s))continue;const i=this._extractFunctions(t,s);e.push(...i)}const n=this._findClusters(e),s=n.reduce((t,e)=>t+e.members.reduce((t,e)=>t+e.lineCount,0)-e.members[0].lineCount,0);return{clusters:n,totalClusters:n.length,totalDuplicateInstances:n.reduce((t,e)=>t+e.members.length,0),totalDuplicateLines:s,recommendation:n.length>0?`Extract ${n.length} shared utilities to eliminate ${s} duplicate lines.`:"No copy-paste drift detected."}}_extractFunctions(t,e){const n=t.split("\n"),s=[];for(let t=0;t<n.length;t++){const i=this._detectFunction(n[t]);if(!i)continue;const a=this._findFunctionEnd(n,t),r=n.slice(t,a+1).join("\n"),l=a-t+1;if(l<this.minLines)continue;const o=this._normalize(r),c=crypto.createHash("md5").update(o).digest("hex"),h=this._tokenize(o);s.push({file:e,name:i.name,line:t+1,lineCount:l,hash:c,tokens:h,normalized:o}),t=a}return s}_findClusters(t){const e=new Set,n=[];for(let s=0;s<t.length;s++){if(e.has(s))continue;const i=[t[s]];e.add(s);for(let n=s+1;n<t.length;n++){if(e.has(n))continue;if(t[s].hash===t[n].hash){i.push(t[n]),e.add(n);continue}const a=this._tokenSimilarity(t[s].tokens,t[n].tokens);a>=this.similarityThreshold&&(i.push({...t[n],similarity:Math.round(100*a)}),e.add(n))}if(i.length>=2){const t=this._findDiffs(i);n.push({pattern:i[0].name,members:i.map(t=>({file:t.file,name:t.name,line:t.line,lineCount:t.lineCount,similarity:t.similarity||100})),totalInstances:i.length,differences:t,severity:i.length>=4?"high":i.length>=3?"medium":"low",recommendation:`Extract to shared utility. ${i.length} functions → 1.`})}}return n.sort((t,e)=>e.totalInstances-t.totalInstances)}_normalize(t){const e=[],n=[/(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,/(?:([a-zA-Z_][a-zA-Z0-9_]*)\s*:=|var\s+([a-zA-Z_][a-zA-Z0-9_]*)|\bfunc\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*))/g,/(?:let\s+(?:mut\s+)?([a-zA-Z_][a-zA-Z0-9_]*)|\bfn\s+([a-zA-Z_][a-zA-Z0-9_]*))/g,/(?:^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=(?!=)|\bdef\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*))/gm,/(?:^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=(?!=)|\bdef\s+([a-zA-Z_][a-zA-Z0-9_]*))/gm];let s;for(const i of n)for(;null!==(s=i.exec(t));){const t=s[1]||s[2]||s[3];t&&!e.includes(t)&&t.length>1&&e.push(t)}let i=t.replace(/\/\/.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"").replace(/#.*$/gm,"").replace(/'[^']*'/g,"STR").replace(/"[^"]*"/g,"STR").replace(/`[^`]*`/g,"STR").replace(/\b\d+\.?\d*\b/g,"NUM");return e.forEach((t,e)=>{const n=new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g");i=i.replace(n,`VAR_${e}`)}),i.replace(/\s+/g," ").trim()}_tokenize(t){return t.split(/[\s{}()\[\];,=+\-*/<>!&|?:.]+/).filter(t=>t.length>0)}_tokenSimilarity(t,e){if(0===t.length&&0===e.length)return 1;if(0===t.length||0===e.length)return 0;const n=new Set(t),s=new Set(e),i=[...n].filter(t=>s.has(t)).length,a=new Set([...t,...e]).size;return.7*(a>0?i/a:0)+Math.min(t.length,e.length)/Math.max(t.length,e.length)*.3}_findDiffs(t){if(t.length<2)return[];const e=[],n=t[0].normalized;for(let s=1;s<t.length;s++){const i=t[s].normalized;if(n!==i){const a=new Set(this._tokenize(n)),r=new Set(this._tokenize(i)),l=[...r].filter(t=>!a.has(t)),o=[...a].filter(t=>!r.has(t));(l.length>0||o.length>0)&&e.push({file:t[s].file,addedTokens:l.slice(0,5),removedTokens:o.slice(0,5)})}}return e}_detectFunction(t){const e=t.trim();let n=e.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=e.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n?{name:n[1]}:(n=e.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=e.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=e.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),n?{name:n[1]}:null))))))}_findFunctionEnd(t,e){const n=(t[e]||"").trim();if(/^def\s/.test(n)){let n=1;for(let s=e+1;s<t.length&&s<e+200;s++){const e=t[s].trim();if(/^(?:def|class|module|begin|case)\b/.test(e)&&n++,/^(?:if|unless|while|until|for)\b/.test(e)&&!/\bthen\b/.test(e)&&n++,/\bdo\s*(\|[^|]*\|)?\s*$/.test(e)&&n++,/^end\b/.test(e)&&n--,n<=0)return s}return Math.min(e+20,t.length-1)}let s=0,i=!1;for(let n=e;n<t.length&&n<e+200;n++){for(const e of t[n])"{"===e&&(s++,i=!0),"}"===e&&s--;if(i&&s<=0)return n}return Math.min(e+20,t.length-1)}_isSkippable(t){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/node_modules/i,/\.min\./i,/\.d\.ts$/,/\.config\./i].some(e=>e.test(t))}}module.exports=CopyPasteDriftDetector;
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CopyPasteDriftDetector{constructor(t={}){this.rootPath=t.rootPath||process.cwd(),this.minLines=t.minLines||5,this.similarityThreshold=t.similarity||.7}scan(t){const e=[];for(const n of t){let t;try{if(fs.statSync(n).size>524288)continue;t=fs.readFileSync(n,"utf-8").replace(/\r\n/g,"\n")}catch(t){continue}const s=path.relative(this.rootPath,n);if(this._isSkippable(s))continue;const i=this._extractFunctions(t,s);e.push(...i)}const n=this._findClusters(e),s=n.reduce((t,e)=>t+e.members.reduce((t,e)=>t+e.lineCount,0)-e.members[0].lineCount,0);return{clusters:n,totalClusters:n.length,totalDuplicateInstances:n.reduce((t,e)=>t+e.members.length,0),totalDuplicateLines:s,recommendation:n.length>0?`Extract ${n.length} shared utilities to eliminate ${s} duplicate lines.`:"No copy-paste drift detected."}}_extractFunctions(t,e){const n=t.split("\n"),s=[];for(let t=0;t<n.length;t++){const i=this._detectFunction(n[t]);if(!i)continue;const a=this._findFunctionEnd(n,t),r=n.slice(t,a+1).join("\n"),l=a-t+1;if(l<this.minLines)continue;const o=this._normalize(r),c=crypto.createHash("md5").update(o).digest("hex"),h=this._tokenize(o);s.push({file:e,name:i.name,line:t+1,lineCount:l,hash:c,tokens:h,normalized:o}),t=a}return s}_findClusters(t){const e=new Set,n=[];for(let s=0;s<t.length;s++){if(e.has(s))continue;const i=[t[s]];e.add(s);for(let n=s+1;n<t.length;n++){if(e.has(n))continue;if(t[s].hash===t[n].hash){i.push(t[n]),e.add(n);continue}const a=this._tokenSimilarity(t[s].tokens,t[n].tokens);a>=this.similarityThreshold&&(i.push({...t[n],similarity:Math.round(100*a)}),e.add(n))}if(i.length>=2){const t=this._findDiffs(i);n.push({pattern:i[0].name,members:i.map(t=>({file:t.file,name:t.name,line:t.line,lineCount:t.lineCount,similarity:t.similarity||100})),totalInstances:i.length,differences:t,severity:i.length>=4?"high":i.length>=3?"medium":"low",recommendation:`Extract to shared utility. ${i.length} functions → 1.`})}}return n.sort((t,e)=>e.totalInstances-t.totalInstances)}_normalize(t){const e=[],n=[/(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,/(?:([a-zA-Z_][a-zA-Z0-9_]*)\s*:=|var\s+([a-zA-Z_][a-zA-Z0-9_]*)|\bfunc\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*))/g,/(?:let\s+(?:mut\s+)?([a-zA-Z_][a-zA-Z0-9_]*)|\bfn\s+([a-zA-Z_][a-zA-Z0-9_]*))/g,/(?:^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=(?!=)|\bdef\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*))/gm,/(?:^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=(?!=)|\bdef\s+([a-zA-Z_][a-zA-Z0-9_]*))/gm];let s;for(const i of n)for(;null!==(s=i.exec(t));){const t=s[1]||s[2]||s[3];t&&!e.includes(t)&&t.length>1&&e.push(t)}let i=t.replace(/\/\/.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"").replace(/#.*$/gm,"").replace(/'[^']*'/g,"STR").replace(/"[^"]*"/g,"STR").replace(/`[^`]*`/g,"STR").replace(/\b\d+\.?\d*\b/g,"NUM");return e.forEach((t,e)=>{const n=new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g");i=i.replace(n,`VAR_${e}`)}),i.replace(/\s+/g," ").trim()}_tokenize(t){return t.split(/[\s{}()\[\];,=+\-*/<>!&|?:.]+/).filter(t=>t.length>0)}_tokenSimilarity(t,e){if(0===t.length&&0===e.length)return 1;if(0===t.length||0===e.length)return 0;const n=new Set(t),s=new Set(e),i=[...n].filter(t=>s.has(t)).length,a=new Set([...t,...e]).size;return.7*(a>0?i/a:0)+Math.min(t.length,e.length)/Math.max(t.length,e.length)*.3}_findDiffs(t){if(t.length<2)return[];const e=[],n=t[0].normalized;for(let s=1;s<t.length;s++){const i=t[s].normalized;if(n!==i){const a=new Set(this._tokenize(n)),r=new Set(this._tokenize(i)),l=[...r].filter(t=>!a.has(t)),o=[...a].filter(t=>!r.has(t));(l.length>0||o.length>0)&&e.push({file:t[s].file,addedTokens:l.slice(0,5),removedTokens:o.slice(0,5)})}}return e}_detectFunction(t){const e=t.trim();let n=e.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=e.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n?{name:n[1]}:(n=e.match(/^func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=e.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=e.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),n?{name:n[1]}:null))))))}_findFunctionEnd(t,e){const n=(t[e]||"").trim();if(/^def\s/.test(n)){let n=1;for(let s=e+1;s<t.length&&s<e+200;s++){const e=t[s].trim();if(/^(?:def|class|module|begin|case)\b/.test(e)&&n++,/^(?:if|unless|while|until|for)\b/.test(e)&&!/\bthen\b/.test(e)&&n++,/\bdo\s*(\|[^|]*\|)?\s*$/.test(e)&&n++,/^end\b/.test(e)&&n--,n<=0)return s}return Math.min(e+20,t.length-1)}let s=0,i=!1;for(let n=e;n<t.length&&n<e+200;n++){for(const e of t[n])"{"===e&&(s++,i=!0),"}"===e&&s--;if(i&&s<=0)return n}return Math.min(e+20,t.length-1)}_isSkippable(t){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/node_modules/i,/\.min\./i,/\.d\.ts$/,/\.config\./i].some(e=>e.test(t))}}module.exports=CopyPasteDriftDetector;
|