diff --git a/.gitignore b/.gitignore index 616fcd0..08012c4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ ccoretrace-sarif-tscancode.json .DS_Store Thumbs.db -# test \ No newline at end of file +# test.vscode-test/ diff --git a/README.md b/README.md index 3e5f940..eeacb66 100644 --- a/README.md +++ b/README.md @@ -43,5 +43,5 @@ No need to wait for a full re-scan. CoreTrace caches file hashes across the enti 4. Click **Run Analysis** to execute `ctrace`. All findings will appear instantly in the panel and directly in your code editor as error/warning highlights. ## ⚙️ Requirements -- The extension automatically uses the `ctrace` / `coretrace` CLI binaries bundled alongside the extension installation. You do not need to install them manually in your PATH. +- The extension automatically downloads and installs the latest `ctrace` / `coretrace` CLI binaries from GitHub Releases during its first activation. You do not need to install them manually in your PATH. It will also periodically check for and download binary updates automatically in the background. - *Recommended:* For accurate analysis in complex codebases, it is advised to generate a `compile_commands.json` (e.g. via `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON`) in your workspace root or `build` directory. diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..3eb4802 Binary files /dev/null and b/icon.png differ diff --git a/media/main.js b/media/main.js index 5ce32bb..d3bdc54 100644 --- a/media/main.js +++ b/media/main.js @@ -30,16 +30,17 @@ // ── Scope selector ───────────────────────────────────────────────────────── let workspaceMode = false; let isRunning = false; + let isDownloading = false; function applyScope(ws) { - if (isRunning) { return; } + if (isRunning || isDownloading) { return; } workspaceMode = !!ws; if (scopeFile) { scopeFile.classList.toggle('active', !workspaceMode); } if (scopeWs) { scopeWs.classList.toggle('active', workspaceMode); } if (filePill) { filePill.classList.toggle('hidden', workspaceMode); } if (wsPill) { wsPill.classList.toggle('hidden', !workspaceMode); } if (wsProgress) { wsProgress.classList.add('hidden'); } - if (runLabel) { runLabel.textContent = 'Run Analysis'; } + if (runLabel && !isDownloading && !isRunning) { runLabel.textContent = 'Run Analysis'; } } if (scopeFile) { scopeFile.addEventListener('click', () => applyScope(false)); } @@ -86,6 +87,15 @@ window.addEventListener('message', event => { const msg = event.data; switch (msg.type) { + case 'analysis-downloading': + setDownloading(msg.progress); + break; + case 'analysis-download-complete': + setDownloadComplete(); + break; + case 'analysis-start': + setRunning(true); + break; case 'analysis-result': handleAnalysisResult(msg.data); break; @@ -107,8 +117,31 @@ }); // ── Handlers ─────────────────────────────────────────────────────────────── + function setDownloading(progressMsg) { + isDownloading = true; + if (!runBtn) { return; } + runBtn.disabled = true; + runBtn.classList.add('running'); // Force spinner instead of play icon + if (runLabel) { runLabel.textContent = `Downloading (${progressMsg})`; } + + if (scopeFile) scopeFile.style.opacity = '0.5'; + if (scopeFile) scopeFile.style.cursor = 'not-allowed'; + if (scopeWs) scopeWs.style.opacity = '0.5'; + if (scopeWs) scopeWs.style.cursor = 'not-allowed'; + } + + function setDownloadComplete() { + isDownloading = false; + if (!isRunning) { + setRunning(false); + } + } + function setRunning(running) { isRunning = running; + if (running) { + isDownloading = false; + } if (!runBtn) { return; } runBtn.disabled = running; runBtn.classList.toggle('running', running); diff --git a/package-lock.json b/package-lock.json index 6b988e8..7b407bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,21 @@ { "name": "ctrace-audit", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctrace-audit", - "version": "0.0.1", + "version": "0.1.0", + "dependencies": { + "axios": "^1.13.6", + "tar": "^7.5.13" + }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", - "@types/vscode": "^1.80.0", + "@types/tar": "^6.1.13", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "lucide": "^0.575.0", @@ -60,6 +65,17 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -149,10 +165,29 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/vscode": { - "version": "1.109.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", - "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true }, "node_modules/@vscode/test-cli": { @@ -279,6 +314,21 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -357,6 +407,18 @@ } } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -421,6 +483,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -538,6 +608,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -599,6 +680,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -608,6 +697,19 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -633,6 +735,47 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -691,6 +834,25 @@ "flat": "cli.js" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -707,6 +869,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -721,6 +898,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -742,6 +927,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -775,6 +995,17 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -790,6 +1021,42 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1108,6 +1375,33 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1139,11 +1433,21 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mocha": { "version": "11.7.5", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", @@ -1451,6 +1755,11 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1732,6 +2041,21 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -2019,6 +2343,14 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 40f9c15..407e81d 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,27 @@ "name": "ctrace-audit", "displayName": "Ctrace Audit", "description": "Integration for Ctrace CLI Audit Tool", - "version": "0.0.1", + "version": "0.1.0", + "publisher": "CoreTrace", + "icon": "icon.png", + "galleryBanner": { + "color": "#1e1e1e", + "theme": "dark" + }, + "keywords": [ + "c", + "cpp", + "security", + "static analysis", + "audit", + "sarif" + ], + "repository": { + "type": "git", + "url": "https://github.com/CoreTrace/coretrace-vscode.git" + }, "engines": { - "vscode": "^1.80.0" + "vscode": "^1.82.0" }, "categories": [ "Other" @@ -56,12 +74,17 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", - "@types/vscode": "^1.80.0", + "@types/tar": "^6.1.13", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "lucide": "^0.575.0", "mocha": "^11.7.5", "ts-node": "^10.9.2", "typescript": "^5.1.3" + }, + "dependencies": { + "axios": "^1.13.6", + "tar": "^7.5.13" } } diff --git a/src/SidebarProvider.ts b/src/SidebarProvider.ts index f838c1f..b677883 100644 --- a/src/SidebarProvider.ts +++ b/src/SidebarProvider.ts @@ -24,6 +24,9 @@ const ALLOWED_COMMANDS = new Set(['ctrace.runAnalysis', 'ctrace.runWorkspaceAnal export type HostMessage = | { type: 'analysis-result'; data: unknown } | { type: 'analysis-error' } + | { type: 'analysis-start' } + | { type: 'analysis-download-complete' } + | { type: 'analysis-downloading'; progress: string } | { type: 'active-file'; name: string | null } | { type: 'workspace-progress'; total: number; changed: number; cached: number; done: number }; diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts new file mode 100644 index 0000000..217a6b0 --- /dev/null +++ b/src/ctrace/BinaryUpdater.ts @@ -0,0 +1,287 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; +import * as tar from 'tar'; +import { pipeline } from 'stream/promises'; +import { locateBinary } from './BinaryLocator'; + +const REPO_LATEST_RELEASE_URL = 'https://api.github.com/repos/CoreTrace/coretrace/releases/latest'; + +let updatePromise: Promise | null = null; + +export function isUpdatingBinary(): boolean { + return updatePromise !== null; +} + +let progressListener: ((msg: string) => void) | undefined; + +export function setBinaryUpdateListener(listener: (msg: string) => void) { + progressListener = listener; +} + +export async function ensureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { + if (updatePromise) { + return updatePromise; + } + updatePromise = doEnsureBinary(context, output).finally(() => { + updatePromise = null; + if (progressListener) { + progressListener('__done__'); + } + }); + return updatePromise; +} + +async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { + const globalStorage = context.globalStorageUri.fsPath; + const binDir = path.join(globalStorage, 'bin'); + + await fs.promises.mkdir(binDir, { recursive: true }); + + const downloadedBinaryPath = await getExtractedBinaryPath(binDir); + const lastCheck = context.globalState.get('coretrace-last-update-check') || 0; + const lastFailedCheck = context.globalState.get('coretrace-last-failed-update-check') || 0; + const now = Date.now(); + const TWELVE_HOURS = 12 * 60 * 60 * 1000; + const FAILURE_BACKOFF = 60 * 1000; + + // If we have a cached binary and checked recently, avoid spamming the GitHub API. + if (downloadedBinaryPath && now - lastCheck < TWELVE_HOURS) { + return downloadedBinaryPath; + } + + // When there is no cached binary, keep retrying with a short backoff. + // This avoids a 12-hour lockout if the very first download fails. + if (!downloadedBinaryPath && now - lastFailedCheck < FAILURE_BACKOFF) { + output.appendLine('Skipping update check due to recent failure backoff.'); + return await locateBinary(context.extensionUri.fsPath); + } + + try { + const response = await axios.get(REPO_LATEST_RELEASE_URL, { + headers: { 'User-Agent': 'vscode-coretrace' }, + timeout: 5000 // Don't block forever + }); + const release = response.data; + const latestVersion = release.tag_name; + + const currentVersion = context.globalState.get('coretrace-version'); + const cachedBinaryPath = await getExtractedBinaryPath(binDir); + + if (latestVersion !== currentVersion || !cachedBinaryPath) { + const assetInfo = getAssetForPlatform(release.assets); + if (assetInfo) { + output.appendLine(`Selected release asset: ${assetInfo.name}`); + if (progressListener) { progressListener("0%"); } + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Downloading CoreTrace ${latestVersion}...`, + cancellable: false + }, async (progress) => { + output.appendLine(`Downloading CoreTrace release ${latestVersion} from GitHub...`); + await downloadAndExtract(assetInfo.url, binDir, progress); + await Promise.all([ + context.globalState.update('coretrace-version', latestVersion), + context.globalState.update('coretrace-last-failed-update-check', undefined) + ]); + output.appendLine(`Updated CoreTrace to ${latestVersion} successfully.`); + }); + } else { + output.appendLine(`No GitHub release asset found for platform ${process.platform} arch ${process.arch}`); + } + } + + // Update the timestamp only after a successful check (and potential download) + await context.globalState.update('coretrace-last-update-check', now); + + const bin = await getExtractedBinaryPath(binDir); + if (bin) return bin; + + } catch (err: any) { + output.appendLine(`Failed to check for CoreTrace updates: ${err.message}`); + await context.globalState.update('coretrace-last-failed-update-check', now); + + // Keep old behaviour only when we already have a working cached binary. + if (downloadedBinaryPath) { + await context.globalState.update('coretrace-last-update-check', now); + } + } + + // Try to return the previously downloaded binary first, even if update check failed + const cachedBin = await getExtractedBinaryPath(binDir); + if (cachedBin) return cachedBin; + + // Fallback to bundled + return await locateBinary(context.extensionUri.fsPath); +} + +function getAssetForPlatform(assets: any[]): { name: string; url: string } | null { + const osMap: Record = { + 'win32': 'windows', + 'linux': 'linux', + 'darwin': 'darwin' + }; + const archMap: Record = { + 'x64': 'amd64', + 'arm64': 'arm64' + }; + + const os = osMap[process.platform]; + const arch = archMap[process.arch]; + if (!os || !arch) return null; + + // First try exact matches (os + arch + .tar.gz) + for (const asset of assets) { + const name = asset.name.toLowerCase(); + if ((name.includes(os) || (process.platform === 'darwin' && name.includes('macos'))) && + name.includes(arch) && + name.endsWith('.tar.gz')) { + return { name: asset.name, url: asset.browser_download_url }; + } + } + // Fallback for older formats (os + .tar.gz) + for (const asset of assets) { + const name = asset.name.toLowerCase(); + if ((name.includes(os) || (process.platform === 'darwin' && name.includes('macos'))) && + name.endsWith('.tar.gz')) { + return { name: asset.name, url: asset.browser_download_url }; + } + } + return null; +} + +async function downloadAndExtract(url: string, destDir: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + const timestamp = Date.now(); + const tarballPath = path.join(destDir, `download-${timestamp}.tar.gz`); + + const token = process.env.GITHUB_TOKEN || ''; + const headers: any = { 'User-Agent': 'vscode-coretrace' }; + if (token) headers['Authorization'] = `token ${token}`; + + const controller = new AbortController(); + let timeoutId = setTimeout(() => controller.abort(new Error("Download stalled")), 30000); + + const response = await axios({ + url, + method: 'GET', + responseType: 'stream', + headers, + signal: controller.signal + }); + + if (response.status !== 200) { + clearTimeout(timeoutId); + throw new Error(`Failed to download asset: HTTP ${response.status}`); + } + + const totalLength = parseInt(response.headers['content-length'], 10); + let downloadedLength = 0; + + const writer = fs.createWriteStream(tarballPath); + + try { + response.data.on('data', (chunk: Buffer) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => controller.abort(new Error("Download stalled")), 30000); + + downloadedLength += chunk.length; + if (totalLength) { + const percent = Math.round((downloadedLength / totalLength) * 100); + const msg = `${percent}%`; + progress.report({ message: msg, increment: (chunk.length / totalLength) * 100 }); + if (progressListener) { progressListener(msg); } + } + }); + + await pipeline(response.data, writer); + clearTimeout(timeoutId); + + if (progressListener) { progressListener("Extracting..."); } + + // Extract to a unique temp directory + const tmpDir = path.join(destDir, `tmp-${timestamp}`); + await fs.promises.mkdir(tmpDir, { recursive: true }); + + try { + await tar.x({ + file: tarballPath, + C: tmpDir, + strip: 0 // Do not strip to avoid dropping root-level binaries + }); + + // Find the extracted binary recursively + const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; + async function findBinary(dir: string): Promise { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const name of candidates) { + const match = entries.find(e => e.isFile() && e.name === name); + if (match) return path.join(dir, match.name); + } + for (const entry of entries) { + if (entry.isDirectory()) { + const res = await findBinary(path.join(dir, entry.name)); + if (res) return res; + } + } + return null; + } + + const binaryInTmp = await findBinary(tmpDir); + if (!binaryInTmp) { + throw new Error("Could not find ctrace/coretrace binary inside the downloaded archive."); + } + + const finalBinPath = path.join(destDir, path.basename(binaryInTmp)); + + // Remove existing binary to avoid errors (e.g., EPERM/EEXIST on Windows) during rename + try { + await fs.promises.unlink(finalBinPath); + } catch (err: any) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + // Move binary to the root of destDir + await fs.promises.rename(binaryInTmp, finalBinPath); + + // Make it executable if on linux/mac + if (process.platform !== 'win32') { + await fs.promises.chmod(finalBinPath, 0o755); + } + } finally { + try { + await fs.promises.rm(tmpDir, { recursive: true, force: true }); + } catch (e) { + // Ignore removal errors + } + } + } finally { + clearTimeout(timeoutId); + try { + await fs.promises.unlink(tarballPath); + } catch (e) { + // Ignore if file doesn't exist or can't be removed + } + } +} + +async function getExtractedBinaryPath(binDir: string): Promise { + const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; + for (const name of candidates) { + // Fallback for flat structure or the newly moved binary + const file = path.join(binDir, name); + if (fs.existsSync(file)) { + return file; + } + + // Tarball structure is often: coretrace-vX.Y.Z-arch/bin/ctrace + // Keeping this for backwards compatibility + const fileInBin = path.join(binDir, 'bin', name); + if (fs.existsSync(fileInBin)) { + return fileInBin; + } + } + return null; +} diff --git a/src/ctrace/SarifParser.ts b/src/ctrace/SarifParser.ts index 89ae0ab..9a081ea 100644 --- a/src/ctrace/SarifParser.ts +++ b/src/ctrace/SarifParser.ts @@ -31,9 +31,15 @@ export async function parseSarifOutput(stdout: string, reportFilePath?: string): } const mergeSarif = (source: SarifLog) => { + if (!source || !source.runs) { + return; + } if (!sarif) { sarif = source; } else { + if (!sarif.runs) { + sarif.runs = []; + } sarif.runs.push(...source.runs); } }; diff --git a/src/extension.ts b/src/extension.ts index 497ae16..33b32eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { SidebarProvider, type HostMessage } from './SidebarProvider'; -import { locateBinary } from './ctrace/BinaryLocator'; +import { ensureBinary, isUpdatingBinary, setBinaryUpdateListener } from './ctrace/BinaryUpdater'; import { buildCommand, parseAndValidateParams } from './ctrace/CommandBuilder'; import { runCommand } from './ctrace/AnalysisRunner'; import { parseSarifOutput, countResults } from './ctrace/SarifParser'; @@ -32,6 +32,19 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.registerWebviewViewProvider('ctrace-audit-view', sidebarProvider) ); + setBinaryUpdateListener((msg) => { + if (msg === '__done__') { + sidebarProvider.postMessage({ type: 'analysis-download-complete' }); + return; + } + sidebarProvider.postMessage({ type: 'analysis-downloading', progress: msg }); + }); + + // Initialise and pre-fetch the binary in the background on startup + ensureBinary(context, output).catch((err) => { + output.appendLine('Failed to pre-fetch binary on activation: ' + err); + }); + // ── Diagnostics collection ─────────────────────────────────────────────── const diagnosticCollection = vscode.languages.createDiagnosticCollection('ctrace'); context.subscriptions.push(diagnosticCollection); @@ -44,10 +57,18 @@ export function activate(context: vscode.ExtensionContext) { // ── Shared helpers ─────────────────────────────────────────────────────── async function locateOrError(): Promise { - const p = await locateBinary(context.extensionUri.fsPath); + let p: string | null = null; + try { + p = await ensureBinary(context, output); + } catch (e: any) { + output.appendLine(`ensureBinary threw an error: ${e.message}`); + } + if (!p) { + const extPath = context.extensionUri.fsPath; + const globalStorage = context.globalStorageUri.fsPath; vscode.window.showErrorMessage( - `Ctrace binary not found in extension folder: ${context.extensionUri.fsPath}` + `Ctrace binary could not be found or downloaded. Checked: \n- ${globalStorage}/bin\n- ${extPath}\nSee the "Ctrace" Output channel for details.` ); } return p; @@ -62,6 +83,13 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('ctrace.runWorkspaceAnalysis', async (arg?: AnalysisParams | string) => { if (isRunning) { vscode.window.showWarningMessage('An analysis is already in progress.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); + return; + } + + if (isUpdatingBinary()) { + vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } @@ -76,6 +104,8 @@ export function activate(context: vscode.ExtensionContext) { sidebarProvider.postMessage({ type: 'analysis-error' }); return; } + + sidebarProvider.postMessage({ type: 'analysis-start' }); const params = resolveParams(arg); @@ -293,16 +323,20 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('ctrace.runAnalysis', async (arg?: AnalysisParams | string) => { if (isRunning) { vscode.window.showWarningMessage('An analysis is already in progress.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); + return; + } + + if (isUpdatingBinary()) { + vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } - isRunning = true; - const params = resolveParams(arg); const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showErrorMessage('No active file to analyse.'); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } @@ -311,21 +345,22 @@ export function activate(context: vscode.ExtensionContext) { if (!vscode.workspace.workspaceFolders?.length) { vscode.window.showErrorMessage('Please open a workspace folder.'); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } // Locate binary - const ctracePath = await locateBinary(context.extensionUri.fsPath); + const ctracePath = await locateOrError(); + if (!ctracePath) { - vscode.window.showErrorMessage( - `Ctrace binary not found in extension folder: ${context.extensionUri.fsPath}` - ); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } + isRunning = true; + sidebarProvider.postMessage({ type: 'analysis-start' }); + + const params = resolveParams(arg); + // Build command (also validates params — throws on unsafe input). // Async on Windows: the fallback path copies the binary to %TEMP% // using non-blocking I/O to avoid stalling the extension host. diff --git a/tsconfig.json b/tsconfig.json index aac2dee..79c8edc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ ], "sourceMap": true, "rootDir": "src", - "strict": true + "strict": true, + "skipLibCheck": true }, "exclude": [ "node_modules",