Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9a6bce5c | ||
|
|
2c0ac71c08 |
@@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"location": "src/config.js:7",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "這裡把 `NODE_TLS_REJECT_UNAUTHORIZED` 全域設為 `0`,等於讓整個 Node 程序放棄 TLS 憑證驗證。攻擊者只要能站到 runner 與 Gitea/LLM/任何 HTTPS API 之間,就能用偽造憑證攔截或竄改 diff、review 結果、token 驗證流程,甚至偷走 Authorization header。",
|
||||||
|
"reason": "內部自架 Gitea/自簽憑證環境的刻意設計(見 config.js 註解)。如需強化可改用 NODE_EXTRA_CA_CERTS,屬人工決策而非誤判。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/config.js:53",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "這個 helper 直接建立 `rejectUnauthorized: false` 的 HTTPS agent,後續 Gitea API 與 preflight 都會用它。攻擊者若能進行中間人攻擊,就能假冒 Gitea 回傳惡意 diff、偽造 comment/review API 回應,或攔截寫入用 token。",
|
||||||
|
"reason": "同 config.js:7,為相容內部自簽憑證的刻意設計;預設不驗證憑證僅限受信任內網使用。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/main.js:2",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "這一行 import 把大量設定常數擠成長長一串,讀起來像沒有換氣的樂句,與後續同檔案多個長 import 一起讓檔案開頭難以掃描。",
|
||||||
|
"reason": "多行 import 排版為風格偏好,非缺陷;本專案採現行單行分組匯入風格。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/findings.js:4",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "這行把四個 prompt/role helper 壓在同一行,與檔案中龐大的流程函式相比,開頭的依賴清單先失了拍,降低可讀性。",
|
||||||
|
"reason": "多行 import 排版為風格偏好,非缺陷。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/gitea.js:2",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "Gitea 設定匯入一口氣列出八個名稱,行寬過長,讓讀者難以快速分辨這個模組真正依賴哪些環境值。",
|
||||||
|
"reason": "多行 import 排版為風格偏好,非缺陷。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/comments.js:11",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "大量私有輔助函式都配上篇幅很長的 JSDoc,許多內容只是重述程式碼表面行為,註解的聲量蓋過了旋律本身。",
|
||||||
|
"reason": "詳盡 JSDoc 為本專案 doc-funcs 流程的刻意文件化風格,非過度註解。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "src/main.js:18",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "main() 前的 JSDoc 幾乎把整條 pipeline 逐步重寫一次,和函式內 Step 註解重複,維護時很容易變成兩份會走調的文件。",
|
||||||
|
"reason": "main() 的 pipeline 概述 JSDoc 為刻意文件化風格,與 Step 註解並存屬設計選擇。"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"level": "critical",
|
|
||||||
"role": "Assassin",
|
|
||||||
"location": "src/config.js:7",
|
|
||||||
"problem": "這裡把 `NODE_TLS_REJECT_UNAUTHORIZED` 全域設為 `0`,等於讓整個 Node 程序放棄 TLS 憑證驗證。攻擊者只要能站到 runner 與 Gitea/LLM/任何 HTTPS API 之間,就能用偽造憑證攔截或竄改 diff、review 結果、token 驗證流程,甚至偷走 Authorization header。",
|
|
||||||
"suggestion": "移除全域停用 TLS 的設定。若內部自簽 CA 是必要情境,請改用可設定的 CA bundle(例如 `NODE_EXTRA_CA_CERTS`)或僅對明確允許的內部 host 使用專用 agent,且預設必須啟用憑證驗證。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "critical",
|
|
||||||
"role": "Assassin",
|
|
||||||
"location": "src/config.js:53",
|
|
||||||
"problem": "這個 helper 直接建立 `rejectUnauthorized: false` 的 HTTPS agent,後續 Gitea API 與 preflight 都會用它。攻擊者若能進行中間人攻擊,就能假冒 Gitea 回傳惡意 diff、偽造 comment/review API 回應,或攔截寫入用 token。",
|
|
||||||
"suggestion": "不要提供預設不驗證憑證的 agent。改成預設安全驗證;若真的要支援自簽憑證,請要求使用者明確提供信任的 CA 憑證,或以白名單 host 加上明確 opt-in 的設定限制風險。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"level": "critical",
|
"level": "critical",
|
||||||
"role": "Assassin",
|
"role": "Assassin",
|
||||||
@@ -39,46 +23,6 @@
|
|||||||
"suggestion": "在分析 prompt 中明確標示 diff 是不可信資料,要求模型忽略 diff 內任何指令;同時加入結構化封裝、輸出 schema 驗證與必要的規則式安全檢查,避免完全依賴可被 prompt injection 操控的 LLM 判斷。",
|
"suggestion": "在分析 prompt 中明確標示 diff 是不可信資料,要求模型忽略 diff 內任何指令;同時加入結構化封裝、輸出 schema 驗證與必要的規則式安全檢查,避免完全依賴可被 prompt injection 操控的 LLM 判斷。",
|
||||||
"is_new": true
|
"is_new": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Bard",
|
|
||||||
"location": "src/main.js:2",
|
|
||||||
"problem": "這一行 import 把大量設定常數擠成長長一串,讀起來像沒有換氣的樂句,與後續同檔案多個長 import 一起讓檔案開頭難以掃描。",
|
|
||||||
"suggestion": "將多項具名 import 改成多行排列,並依來源模組分組維持一致節奏,例如每個匯入項目獨立一行。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Bard",
|
|
||||||
"location": "src/findings.js:4",
|
|
||||||
"problem": "這行把四個 prompt/role helper 壓在同一行,與檔案中龐大的流程函式相比,開頭的依賴清單先失了拍,降低可讀性。",
|
|
||||||
"suggestion": "改為多行具名 import,讓每個 helper 名稱清楚露出,並與其他長 import 採相同格式。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Bard",
|
|
||||||
"location": "src/gitea.js:2",
|
|
||||||
"problem": "Gitea 設定匯入一口氣列出八個名稱,行寬過長,讓讀者難以快速分辨這個模組真正依賴哪些環境值。",
|
|
||||||
"suggestion": "將具名 import 拆成多行,必要時依 token、repo/PR、TLS helper 等語意排序。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Bard",
|
|
||||||
"location": "src/comments.js:11",
|
|
||||||
"problem": "大量私有輔助函式都配上篇幅很長的 JSDoc,許多內容只是重述程式碼表面行為,註解的聲量蓋過了旋律本身。",
|
|
||||||
"suggestion": "保留公開 API 或非直覺決策的文件即可;私有小函式改用簡短註解,或讓函式命名本身說明用途。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Bard",
|
|
||||||
"location": "src/main.js:18",
|
|
||||||
"problem": "main() 前的 JSDoc 幾乎把整條 pipeline 逐步重寫一次,和函式內 Step 註解重複,維護時很容易變成兩份會走調的文件。",
|
|
||||||
"suggestion": "縮短為高階摘要與退出規則;Step 細節留在程式碼附近,避免文件與實作雙重維護。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"level": "warning",
|
"level": "warning",
|
||||||
"role": "Leo",
|
"role": "Leo",
|
||||||
@@ -103,30 +47,6 @@
|
|||||||
"suggestion": "排除條目與 finding 應使用同一套正規化函式比對;例如改存並使用 `normalizeText(ex.text || ex.suggestion || ex.title || '')`,或讓 finding 也轉成同樣的 compact/lowercase key。",
|
"suggestion": "排除條目與 finding 應使用同一套正規化函式比對;例如改存並使用 `normalizeText(ex.text || ex.suggestion || ex.title || '')`,或讓 finding 也轉成同樣的 compact/lowercase key。",
|
||||||
"is_new": true
|
"is_new": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Mage",
|
|
||||||
"location": "src/main.js:221",
|
|
||||||
"problem": "若 Step6 的 `cloneRepo()` 失敗,`repoDir` 會是 undefined,但這裡仍呼叫 `commitAndPush(WORKSPACE, repoDir || WORKSPACE, ...)`。最小重現:遠端 clone 因分支不存在或網路錯誤失敗後,流程降級繼續,最後 Step10 會在 `/workspace` 這個非 git repo 執行 `git config/status/commit`,錯誤只被 `commitAndPush` 吞掉;findings/exclusions 已發布但不會被持久化到 PR 分支,下一輪會遺失記憶狀態。",
|
|
||||||
"suggestion": "Step10 應在 `repoDir` 不存在時明確跳過 commit/push 並標記持久化失敗,或讓 clone 失敗成為會終止流程的錯誤;不要把 WORKSPACE 當成 repoDir fallback。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Mage",
|
|
||||||
"location": "src/resolve.js:92",
|
|
||||||
"problem": "對話分組行號只讀 `position` 或 `original_position`,但新增留言發布時使用的是 `new_position`。若 Gitea 回傳 review comment 只帶 `new_position`,最小重現:同一檔案第 10 行與第 20 行兩則未解決 bot comment 都沒有 `position`,兩者會被合併成 `file|0`,只解析第一個 finding,後續 resolved/open/false_positive 判斷會套錯問題。",
|
|
||||||
"suggestion": "分組行號應納入 `new_position`,例如 `Number(c?.position) || Number(c?.new_position) || Number(c?.original_position) || 0`,並針對缺行號的 comment 避免把同檔不同對話合併成同一組。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Mage",
|
|
||||||
"location": "src/findings.js:336",
|
|
||||||
"problem": "AI 去重回傳只要是非空陣列就被接受,沒有檢查是否比原始 findings 更多。最小重現:原本 3 筆 findings,LLM 異常回傳 20 筆或加入不存在的 location,這裡會直接採用並進入發布與失敗判定,導致憑空產生問題或讓 workflow 誤失敗。",
|
|
||||||
"suggestion": "去重結果應驗證每筆都能對應回原始 finding,且數量不得大於輸入;無法對應或數量異常時應降級回原始 findings,或只保留 `origMap` 命中的項目。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"level": "warning",
|
"level": "warning",
|
||||||
"role": "Maya",
|
"role": "Maya",
|
||||||
|
|||||||
+11
-6
@@ -420,13 +420,18 @@ export async function deduplicateWithAI(findings) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings)));
|
const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings)));
|
||||||
if (Array.isArray(result) && result.length > 0) {
|
// 去重結果數量不得超過輸入(避免 LLM 無中生有),且每筆都必須能對應回原始 finding。
|
||||||
ok(`AI 去重: ${findings.length} -> ${result.length} 筆`);
|
if (Array.isArray(result) && result.length > 0 && result.length <= findings.length) {
|
||||||
// 以 location+suggestion 為 key,將原始 findings 的完整欄位(含 is_new)補回
|
const keyOf = f => `${f.location}|${String(f.suggestion).slice(0, 50)}`;
|
||||||
const origMap = new Map(findings.map(f => [`${f.location}|${String(f.suggestion).slice(0, 50)}`, f]));
|
const origMap = new Map(findings.map(f => [keyOf(f), f]));
|
||||||
return result.map(r => origMap.get(`${r.location}|${String(r.suggestion).slice(0, 50)}`) ?? r);
|
// 只保留能對應回原始 finding 的項目,丟棄無法對應(可能為幻覺)的結果
|
||||||
|
const mapped = result.map(r => origMap.get(keyOf(r))).filter(Boolean);
|
||||||
|
if (mapped.length > 0) {
|
||||||
|
ok(`AI 去重: ${findings.length} -> ${mapped.length} 筆`);
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error('AI 回傳空陣列');
|
throw new Error('AI 去重結果異常(空、超量或無法對應原始 findings)');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return fallback('AI 去重', findings, e);
|
return fallback('AI 去重', findings, e);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -209,7 +209,13 @@ async function main() {
|
|||||||
step('Step10', '記憶區 Commit/Push');
|
step('Step10', '記憶區 Commit/Push');
|
||||||
const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success';
|
const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success';
|
||||||
input(`review outcome=${reviewOutcome}`);
|
input(`review outcome=${reviewOutcome}`);
|
||||||
await commitAndPush(WORKSPACE, repoDir || WORKSPACE, undefined, undefined, reviewOutcome);
|
// clone 失敗(repoDir 為 undefined)時不可把 WORKSPACE(非來源分支 git repo)當 repoDir,
|
||||||
|
// 否則會在錯誤的工作目錄嘗試 commit/push,findings/exclusions 無法持久化到 PR 分支。
|
||||||
|
if (!repoDir) {
|
||||||
|
warn('來源分支 clone 失敗,略過 findings/exclusions 持久化(不以 WORKSPACE 當 repoDir)');
|
||||||
|
} else {
|
||||||
|
await commitAndPush(WORKSPACE, repoDir, undefined, undefined, reviewOutcome);
|
||||||
|
}
|
||||||
|
|
||||||
// Step11 嚴重問題把關
|
// Step11 嚴重問題把關
|
||||||
step('Step11', '嚴重問題把關');
|
step('Step11', '嚴重問題把關');
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ export function groupConversations(comments) {
|
|||||||
for (const c of comments || []) {
|
for (const c of comments || []) {
|
||||||
const filePath = typeof c?.path === 'string' ? c.path : '';
|
const filePath = typeof c?.path === 'string' ? c.path : '';
|
||||||
if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組
|
if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組
|
||||||
const lineNum = Number(c?.position) || Number(c?.original_position) || 0;
|
const lineNum = Number(c?.position) || Number(c?.new_position) || Number(c?.original_position) || 0;
|
||||||
const key = `${filePath}|${lineNum}`;
|
const key = `${filePath}|${lineNum}`;
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
groups.set(key, { key, path: filePath, line: lineNum, commentIds: [], bodies: [], resolved: false, botFinding: null });
|
groups.set(key, { key, path: filePath, line: lineNum, commentIds: [], bodies: [], resolved: false, botFinding: null });
|
||||||
|
|||||||
Reference in New Issue
Block a user