Files
release-cleanup/.gitea/ai-review/findings.json
T

171 lines
11 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[
{
"level": "critical",
"role": "Assassin",
"location": "app/releases.js:48",
"problem": "攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列(如 '../'),進而操控 API 的刪除目標,造成越權刪除其他儲存庫的成品。",
"suggestion": "在將 id 拼接進 URL 前,務必使用 encodeURIComponent(id) 對 id 進行編碼,確保其僅被視為路徑的一部分,而非路徑控制字元。",
"is_new": true
},
{
"level": "critical",
"role": "Assassin",
"location": "app/tags.js:70",
"problem": "攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列,進而操控 API 的刪除目標;同時,tag 名稱若包含惡意字元也可能造成路徑穿越。",
"suggestion": "同樣地,在將 tag.name 拼接進 URL 前,必須使用 encodeURIComponent(tag.name) 對其進行編碼。",
"is_new": true
},
{
"level": "critical",
"role": "Maya",
"problem": "main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。",
"suggestion": "請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。",
"location": "app/index.js:31",
"is_new": false
},
{
"level": "critical",
"role": "Maya",
"location": "app/test/config.test.js:63",
"problem": "測試只驗證了正常與極端的 token 輸入,但完全沒有測試 token 為 null 或未定義(匿名模式)下的處理邏輯,也沒確認匿名請求時,設定物件是否正確地將 token 設為 null。",
"suggestion": "請增加測試案例,明確斷言當 GITEA_TOKEN 不存在於環境變數時,loadConfig() 回傳的 token 屬性為 null。"
},
{
"level": "critical",
"role": "Maya",
"location": "app/test/gitea-client.test.js:33",
"problem": "測試中雖然有 fetchAllPages 的功能測試,但對於 MAX_PAGES 的邊界情況,測試只用了 globalThis.fetch 永遠回傳非空陣列,這是一個快樂路徑的極端變體。如果 API 剛好在第 MAX_PAGES 頁回傳空陣列,測試並未驗證客戶端是否能正確處理並停止。",
"suggestion": "請增加一個測試案例,模擬當 API 恰好在第 MAX_PAGES 次請求時回傳空陣列的情境,確認客戶端能成功結束,而非拋出例外。"
},
{
"level": "warning",
"role": "Mage",
"location": "app/releases.js:38",
"problem": "清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。",
"suggestion": "引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。",
"status": "deferred",
"defer_reason": "與 app/releases.js:46 同屬錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。",
"is_new": false
},
{
"level": "warning",
"role": "Leo",
"location": "app/releases.js:37",
"problem": "cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則。",
"suggestion": "拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。",
"status": "deferred",
"defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑與編碼測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。",
"is_new": false
},
{
"level": "warning",
"role": "Rogue",
"location": "app/releases.js:43",
"problem": "清理成品與刪除 tag 使用序列化迴圈,API 請求逐一排隊,整體執行時間拉長。",
"suggestion": "改用 Promise.all 搭配 map 將刪除請求並行化。",
"status": "deferred",
"defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。",
"is_new": false
},
{
"level": "warning",
"role": "Assassin",
"location": "app/gitea-client.js:66",
"problem": "錯誤路徑以 res.text() 讀取整個回應主體,未限制大小,惡意伺服器可回傳極大內容導致記憶體耗盡(DoS)。",
"suggestion": "限制讀取的回應大小,例如檢查 Content-Length 或以串流方式設定讀取上限。",
"status": "deferred",
"defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需以串流逐段讀取並設位元組上限(Content-Length 在 chunked 回應下不可靠),屬較大改動,保留待人工評估。",
"is_new": false
},
{
"level": "warning",
"role": "Assassin",
"location": "app/gitea-client.js:77",
"problem": "成功路徑以 res.json() 解析整個回應,未限制大小,惡意伺服器可回傳極大 JSON 導致記憶體耗盡(DoS)。",
"suggestion": "對 API 回應設定明確大小上限,超過時拒絕解析並拋出異常。",
"status": "deferred",
"defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。",
"is_new": false
},
{
"level": "warning",
"role": "Maya",
"problem": "logger.js 中的輸出函式(如 separator, section, info, success, warn, fail)負責 Action 的核心視覺輸出格式,但目前缺乏測試驗證其實際輸出內容是否正確對齊並符合格式要求。",
"suggestion": "請在 app/test/logger.test.js 中補齊對這些函式的測試,驗證其是否正確寫入預期的格式內容(含分隔線與正確的前綴)到 stdout。",
"location": "app/logger.js:9",
"is_new": false
},
{
"level": "warning",
"role": "Maya",
"problem": "sanitizeBody 函式負責 API 回應的字串清理與格式化,但目前缺乏直接的單元測試,無法確保正規表示式能正確處理所有控制字元以及長度限制。",
"suggestion": "請在 app/test/gitea-client.test.js 中新增 sanitizeBody 的單元測試,務必包含正常字串、包含控制字元的字串、空字串/null 值、以及超過 200 字元的極端案例。",
"location": "app/gitea-client.js:13",
"is_new": false
},
{
"level": "warning",
"role": "Mage",
"location": "app/releases.js:59",
"problem": "使用字串模板直接拼接 API URL`const url = `${config.releaseApiUrl}/${id}`;`。若 API 回傳的 id 包含 `/` 或特殊字元,可能導致拼接出錯誤的 API 路徑,甚至在某些處理器上導致路徑穿越,雖是 Gitea API 但應防禦性地進行 URL 編碼。",
"suggestion": "建議使用 encodeURIComponent(id) 對 id 進行編碼後再拼接。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "app/tags.js:65",
"problem": "與 releases.js 相同問題,在拼接 tag URL 時:`const url = `${config.tagApiUrl}/${tag.name}`;` 未對 tag 名稱進行 URL 編碼。Tag 名稱若包含 `/` 等特殊字元,會破壞 URL 結構。",
"suggestion": "建議使用 encodeURIComponent(tag.name) 對 tag 名稱進行編碼。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "app/test/releases.test.js:77",
"problem": "在 `cleanupReleases` 的整合測試中,雖然有測試 `deleteResource` 回傳非 204 時的行為,但測試只檢查了 `client.deleted` 的呼叫順序,並沒有驗證 `fail` 日誌(對 stderr 的寫入)是否正確被觸發。",
"suggestion": "建議攔截 `process.stderr.write`,確認當 `deleteResource` 回傳 500 時,系統確實有記錄到錯誤訊息。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "app/test/tags.test.js:73",
"problem": "在 `cleanupOrphanTags` 的測試中,缺乏對於「當 `deleteResource` 失敗」時的行為驗證。目前只測試了成功的情境。",
"suggestion": "增加模擬 `deleteResource` 回傳非 204 狀態碼的情境,確認該項刪除失敗不會中斷整個標籤清理流程,並檢查是否有對應的錯誤日誌。",
"is_new": true
},
{
"level": "info",
"role": "Bard",
"location": "app/releases.js:28",
"problem": "在 `cleanupReleases` 函式中,參數 `config` 是 `ReturnType<import('./config.js').loadConfig>`,這種型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。",
"suggestion": "建議在 `app/config.js` 定義並匯出型別註解(JSDoc @typedef),然後在此處直接使用該別名,提升整體程式碼的可讀性。",
"is_new": true
},
{
"level": "info",
"role": "Bard",
"location": "app/gitea-client.js:25",
"problem": "在 `GiteaClient` 建構子中,`this.headers` 初始化時僅簡單檢查 `token` 是否存在,且假設所有請求都適用這組標頭。雖然目前專案單純,但若未來擴充需針對不同 API 採取不同標頭時,此處結構會稍顯死板。",
"suggestion": "考慮將產生 header 的邏輯抽離成一個內部 private 函式(如 `_getHeaders()`),即便現在很簡單,也能增加未來的彈性。",
"is_new": true
},
{
"level": "info",
"role": "Maya",
"location": "app/test/validate.test.js:20",
"problem": "雖然有 `requireInteger` 的測試,但沒有測試當 `KEEP_COUNT` 為 `0` 時的邊界情況。雖然 0 在邏輯上可能是允許的,但這對於清理邏輯來說是個關鍵的邊界。",
"suggestion": "明確測試 `requireInteger('KEEP_COUNT', '0')` 並斷言其不應拋出錯誤,確保系統允許保留 0 個成品的配置。",
"is_new": true
},
{
"level": "info",
"role": "Rogue",
"location": "app/releases.js:23",
"problem": "為了排序而進行了多次 map 操作,先將陣列 map 為物件陣列,排序後又 map 回原始物件陣列。這在大數據集下會造成多次 O(n) 的陣列分配與垃圾回收壓力,浪費 CPU 與記憶體週期。",
"suggestion": "排序時應嘗試減少陣列中間狀態的產生,例如考慮使用原地排序(若不介意原陣列變更)或簡化 mapping 的邏輯。",
"is_new": true
}
]