Day 23,我們讓 Challenger Agent 對三個 Candidate 進行獨立驗證。
最後得到:
AUTH-01 CONFIRMED
SECRET-01 REJECTED
RATE-01 REQUIRES_EVIDENCE
如果只把 AUTH-01 寫進最終報告,當下看起來沒有問題。
但隔天再執行一次 Review,我們會遇到更多問題:
這是昨天的 AUTH-01,還是新的漏洞?
程式碼移動後,是否會建立重複 Finding?
誰正在修正?
修正後有沒有重新測試?
RATE-01 還缺哪些正式環境證據?
SECRET-01 為什麼被拒絕?
這次結果由哪個版本的 Agent 與 Rule 產生?
一篇自然語言報告可以讓人閱讀,卻不適合支援這些查詢。
今天要把 Finding 從一次性的模型回答,改造成具有:
的結構化資料。
這份 Schema 會成為 Day 25 CLI、Day 26 Pull Request 檢查,以及 Day 27 評估資料的共同 Contract。
最簡單的 AI Review Output 可能長這樣:
{
"severity": "high",
"description": "Firestore Rules may allow unauthorized access."
}
它適合 Demo,卻無法支援正式流程。
第一個問題是沒有穩定身分。
下次模型可能改寫成:
Authenticated users can access projects owned by other accounts.
人類知道兩句話可能指向同一個問題,程式卻無法只靠文字可靠判斷。
第二個問題是沒有狀態。
這個 Finding 可能:
剛確認
等待補證據
已分派
修正中
已修正
接受風險
確認為誤報
如果每次只重新輸出一段摘要,前一次狀態就會消失。
第三個問題是沒有執行來源。
我們不知道它來自:
哪一次 Run
哪個 Revision
哪個 Rule Version
哪個 Tool Version
哪個 Model
哪個時間點
因此正式 Finding 至少要同時回答三件事:
它是什麼?
我們為什麼相信它?
現在應該如何處理?
今天的資料模型分成三層。
第一層是 Run。
它描述這次分析:
{
"id": "run-2026-09-12T03:30:00.000Z",
"repository": "vibe-guard-demo",
"revision": "day-24-demo",
"startedAt": "2026-09-12T03:30:00.000Z",
"completedAt": "2026-09-12T03:31:12.000Z",
"tool": {
"name": "Vibe Guard",
"version": "0.24.0",
"model": "gemini-structured-reviewer"
}
}
第二層是 Finding。
它描述跨 Run 持續追蹤的問題:
AUTH-01
SEC-AUTHZ-001
firestore.rules
Confirmed / High
Resolved
第三層是 Status Event。
它描述 Finding 如何改變:
OPEN
→ IN_PROGRESS
→ RESOLVED
如果只保存 Finding 的目前狀態,我們會知道它現在是 resolved,卻不知道:
因此目前狀態用來查詢,Event History 用來稽核。
今天每個 Finding 同時有:
{
"id": "AUTH-01",
"fingerprint": "sha256:..."
}
id 是人類溝通使用的識別碼。
它適合出現在:
Issue
Pull Request
報告
會議
修正紀錄
fingerprint 則讓系統判斷不同 Run 的結果是否為同一個邏輯問題。
Demo 使用:
function fingerprint(ruleId, path, identity) {
const digest = createHash("sha256")
.update(`${ruleId}\0${path}\0${identity}`)
.digest("hex");
return `sha256:${digest}`;
}
輸入包含:
Rule ID
穩定的相對 Path
問題的邏輯身分
不要直接把完整訊息或絕對行號當成唯一身分。
訊息可能被模型改寫,行號也會隨著新增程式碼移動。
GitHub Code Scanning 的 SARIF 支援同樣強調 Fingerprint 與一致 Path。當新分析結果上傳時,Code Scanning 會利用 Fingerprint 對應不同 Run 的相同問題,避免每次都建立新的 Alert。
不過,Fingerprint 不是越穩定越好。
如果只使用:
SEC-AUTHZ-001
同一條 Rule 在十個不同位置找到的問題會全部被合併。
如果把:
完整程式碼內容
絕對行號
暫存目錄
模型描述
全部放進 Fingerprint,任何小改動都會建立新的 Finding。
Fingerprint 的目標是穩定辨認同一個問題,不是替整份 Finding 做內容雜湊。
verdict 回答:
目前證據支持什麼結論?
今天保留三種:
| Verdict | 意義 |
|---|---|
confirmed |
攻擊路徑、影響與驗證已成立 |
requires_evidence |
仍缺少足以確認或推翻的資料 |
rejected |
核心主張已被反證 |
status 回答:
這個項目目前位於哪個處理階段?
今天使用:
| Status | 意義 |
|---|---|
open |
已確認,尚未開始處理 |
awaiting_evidence |
等待指定證據 |
in_progress |
已分派並處理中 |
resolved |
修正與回歸測試已完成 |
accepted_risk |
已由有權責的人接受風險 |
false_positive |
原始主張已被拒絕 |
兩者不能混在一起。
例如:
confirmed + open
confirmed + in_progress
confirmed + resolved
confirmed + accepted_risk
都可能是合法組合。
confirmed 不代表「已經修好」,只代表「問題成立」。
同樣地:
requires_evidence + awaiting_evidence
不是低風險 Finding。
它代表目前無法可靠指定 Severity,也不能假裝檢查已完成。
Day 23 的 RATE-01 缺少正式部署架構、Edge Control 與 Load Test。
如果 Schema 強迫每個 Finding 都有 Severity,模型可能輸出:
{
"verdict": "requires_evidence",
"severity": "high"
}
這會讓「Hunter 最初宣稱 High」看起來像已驗證結果。
今天將 Severity 設計成可以為 null:
severity: severitySchema.nullable()
因此 RATE-01 可以明確表示:
{
"verdict": "requires_evidence",
"status": "awaiting_evidence",
"severity": null,
"confidence": "low"
}
null 不是漏填。
它表示:
目前證據不足,不能可靠指定嚴重度。
對 SECRET-01 也是如此。
既然核心漏洞主張已被拒絕,就不應保留原本的 High,也不應降成 Low 假裝仍有一個較小漏洞。
Day 20 的 reproduction 還是一段文字。
今天改成結構化 Verification Record:
{
"id": "verify-auth-exploit",
"type": "exploit",
"result": "passed",
"command": "npm run authz:demo",
"artifact": "adversarial-validation-demo/output/challenges.json",
"executedAt": "2026-09-12T03:30:20.000Z",
"environment": "Firebase Firestore Emulator"
}
每筆紀錄都回答:
result 使用:
passed
failed
unknown
這裡的 passed 表示該驗證成功支持它要測試的主張。
例如:
exploit / passed
代表 Exploit Test 成功重現問題。
而:
regression / passed
代表修正後的 Regression Test 成功阻擋問題。
只看 passed 不夠,必須連同 type 一起解讀。
AUTH-01 先執行不安全版本:
{
"type": "exploit",
"result": "passed",
"command": "npm run authz:demo"
}
修正後再執行安全版本:
{
"type": "regression",
"result": "passed",
"command": "npm run authz:secure"
}
第二筆紀錄不能覆蓋第一筆。
否則最後只會看到:
Test Passed
卻不知道它代表:
漏洞曾成功重現
修正後攻擊已被阻擋
正式系統應將 Verification 視為不可變紀錄。新的測試附加新項目,而不是修改過去的執行事實。
如果 Artifact 會存放很久,還應加入:
Exit Code
Duration
Tool Version
Fixture Version
Artifact Digest
Container Image
Operating System
今天先保留支援生命週期判斷的最小欄位。
最危險的狀態轉換是:
工程師說已修正
→ Finding 直接變成 Resolved
「程式碼已修改」不代表漏洞已被阻擋。
今天的 Schema 要求 resolved 同時具備:
remediation.status = fixed
remediation.fixedAt != null
至少一筆 passed regression verification
Zod 的語意驗證如下:
if (finding.status === "resolved") {
const hasPassedRegression = finding.verification.some(
(record) =>
record.type === "regression" &&
record.result === "passed"
);
if (
finding.remediation.status !== "fixed" ||
finding.remediation.fixedAt === null ||
!hasPassedRegression
) {
context.addIssue({
code: "custom",
message:
"resolved finding requires a fixed timestamp and passed regression"
});
}
}
這項限制讓 Reporter、CLI 或 Pull Request Check 不能只靠一句:
Fixed.
就把 Finding 結案。
Day 20 已有 mitigation,但它只是一段建議。
今天擴充成:
{
"owner": "platform-team",
"status": "fixed",
"recommendation": "Compare request.auth.uid with ownerId and prevent ownership changes.",
"pullRequest": "https://github.com/example/vibe-guard-demo/pull/24",
"fixedAt": "2026-09-12T03:31:02.000Z"
}
這裡刻意分開:
recommendation
與:
實際由誰處理、是否開始、哪個 PR 修正、何時完成
Agent 可以提出 Recommendation。
但 owner、pullRequest 與 fixedAt 應來自 Workflow、Repository 或 Issue Tracker,而不是讓模型自行發明。
對 RATE-01,正確的 Remediation 不是立刻加入 Express Middleware:
{
"owner": "sre-team",
"status": "not_started",
"recommendation": "Collect deployment-edge controls before assigning severity or code changes.",
"pullRequest": null,
"fixedAt": null
}
因為目前還不知道正式 Request 是否經過 CDN、Gateway、Quota 或其他 Edge Control。
缺少證據時先建立取證工作,不要假裝所有問題都能用改 Source Code 解決。
AUTH-01 的完整狀態為:
[
{
"sequence": 1,
"from": null,
"to": "open",
"actor": "validator-agent",
"reason": "Dynamic exploit confirmed cross-account read and write.",
"at": "2026-09-12T03:30:20.000Z"
},
{
"sequence": 2,
"from": "open",
"to": "in_progress",
"actor": "platform-team",
"reason": "Owner-checking rules assigned for remediation.",
"at": "2026-09-12T03:30:35.000Z"
},
{
"sequence": 3,
"from": "in_progress",
"to": "resolved",
"actor": "validator-agent",
"reason": "Secure rules denied attacker read and write.",
"at": "2026-09-12T03:31:02.000Z"
}
]
Schema 會檢查:
sequence 必須從 1 連續增加。from 必須是 null。from 必須等於前一筆 to。status 必須等於最後一筆 Event 的 to。因此以下資料會被拒絕:
OPEN
→ RESOLVED
目前 status 卻寫成 IN_PROGRESS
狀態歷史也不能只記:
system updated finding
actor 與 reason 必須具體指出誰做了什麼判斷。
正式環境還需要限制不同 Actor 的權限。
例如:
Validator Agent 可以確認技術驗證結果。
工程團隊可以接受修正工作。
指定風險負責人才可以 accepted_risk。
Reporter 不能改變任何狀態。
Schema 能驗證資料形狀與部分不變條件,不能取代 Authorization。
JSON Schema 適合限制:
Object、Array 與欄位結構
String、Number、Boolean 與 Null
Required Property
Enum
Pattern
最小與最大值
是否允許額外欄位
但今天還需要跨欄位規則:
confirmed 必須有 passed exploit
requires_evidence 必須列出 missingEvidence
rejected 必須使用 false_positive
resolved 必須有 fixedAt 與 passed regression
status 必須等於最後一筆 Event
Event 的 from/to 必須連續
因此 Demo 使用 Zod superRefine 執行 Application-level Validation。
這與 Day 20 的分層原則相同:
| 層次 | 責任 |
|---|---|
| Structured Output / JSON Schema | 限制模型輸出形狀 |
| Zod | 驗證 Application Runtime 資料 |
| Semantic Invariant | 檢查跨欄位生命週期規則 |
| Evidence Validator | 核對 Source、Artifact 與 Runtime |
| Authorization | 限制誰能改變狀態 |
一份資料通過 Zod,不代表其中的測試真的執行過。
artifact 指向的檔案仍要存在,Digest 仍要核對,執行環境也要能追蹤。
專案新增:
demo-app/
└── findings-schema-demo/
├── schema.js
├── scenario.js
└── output/
├── findings-report.json
└── status-summary.json
package.json 新增:
{
"scripts": {
"findings-schema:demo": "node findings-schema-demo/scenario.js"
}
}
執行:
cd /media/mickey/777/ithome/demo-app
npm run findings-schema:demo
Scenario 會建立 Day 23 的三項結果。
AUTH-01:
verdict = confirmed
status = resolved
severity = high
它同時保存 Exploit 與 Regression Test。
SECRET-01:
verdict = rejected
status = false_positive
severity = null
它保存被 Runtime Guard 與 Emulator 設定推翻的紀錄,而不是從資料集中刪除。
RATE-01:
verdict = requires_evidence
status = awaiting_evidence
severity = null
它列出三項缺少的正式環境證據。
實際輸出:
TRACKABLE FINDINGS SCHEMA
PASS report: 3 findings, schema=2.0.0
AUTH-01 verdict=confirmed status=resolved
SECRET-01 verdict=rejected status=false_positive
RATE-01 verdict=requires_evidence status=awaiting_evidence
REJECT resolved without regression
resolved finding requires a fixed timestamp and passed regression
Artifacts: findings-schema-demo/output/
Demo 還會刻意建立一份錯誤資料。
它保留:
status = resolved
remediation.status = fixed
fixedAt 已填寫
但刪除 Regression Verification。
Schema 因此拒絕這份資料。
這個測試很重要,因為它驗證的是:
真正的完成條件
而不是只確認 JSON 可以被解析。
Demo 會產生:
{
"awaiting_evidence": [
"RATE-01"
],
"false_positive": [
"SECRET-01"
],
"resolved": [
"AUTH-01"
]
}
這份摘要由程式根據已驗證資料建立。
不需要再呼叫模型問:
請摘要目前有哪些問題已完成、哪些等待證據。
能用 Deterministic Code 完成的聚合,就不要交給 LLM。
相同資料還可以直接產生:
Open Findings
Awaiting Evidence Queue
Resolved This Week
False Positive Ratio
Mean Time to Remediate
Rule-level Rejection Rate
如果 Schema 穩定,Dashboard、CLI、Pull Request Check 與評估工具都可以重用同一份資料。
SARIF 2.1.0 是靜態分析結果交換標準。
它可以描述:
Tool 與 Rule
Analysis Run
Result
Source Location
Code Flow
Fingerprint
Baseline State
Fix
Suppression
GitHub Code Scanning 也可以接收第三方工具產生的 SARIF,將結果顯示成 Repository Alert。
因此 Vibe Guard 最後應該支援 SARIF Export。
但今天仍先建立內部 Findings Schema,原因有三個。
第一,Vibe Guard 不只處理靜態程式碼結果。
它還保存:
Firestore Emulator 執行
部署證據缺口
人工接受風險
修正工作狀態
Agent 挑戰與反證
第二,內部 Workflow 需要比上傳格式更完整的生命週期資料。
第三,外部格式不應反過來綁死 Agent 內部 Contract。
比較安全的做法是:
Canonical Findings Schema
→ GitHub SARIF Exporter
→ Markdown Reporter
→ Terminal Summary
→ Evaluation Dataset
每個 Exporter 只轉換它需要的欄位。
不要讓 Reporter 重新判斷 Verdict,也不要讓 SARIF Exporter 補寫缺少的 Evidence。
今天的頂層資料使用:
{
"schemaVersion": "2.0.0"
}
Day 20 的最小 Review Contract 是 1.0.0。
Day 24 加入大量生命週期欄位,因此使用新的 Major Version。
Consumer 應明確處理:
支援 2.x
拒絕未知 Major Version
必要時執行 Migration
不要在讀取失敗時偷偷套用預設值。
例如舊資料沒有 statusHistory,不能直接假設:
status = open
因為它可能其實已修正、已接受風險或已被拒絕。
Migration 必須保留:
原始版本
轉換版本
轉換時間
無法推導的欄位
無法可靠推導時,應標記需要人工確認,而不是產生看似完整的假資料。
今天的 Demo 已能驗證單一檔案中的 Findings Report,但正式版本還需要:
accepted_risk、resolved 或重新開啟 Finding。new、unchanged、updated、absent 等 Baseline State。Fingerprint 也需要更多實測。
今天使用固定 Rule、Path 與 Logical Identity。
正式版本應建立測試,確認:
只新增註解,不建立新 Finding。
程式碼移動後,仍能合理對應。
同一 Rule 的不同漏洞,不會錯誤合併。
問題修正後重新出現,可以重新開啟而不是建立無關 ID。
結構化 Findings Schema 的目標,不是把模型作文改成比較漂亮的 JSON。
它要讓每項 Finding 可以被:
辨認
驗證
分派
修正
重測
結案
重新開啟
統計
稽核
匯出
今天建立的核心規則是:
id 用於人類溝通,fingerprint 用於跨 Run 對應。verdict 表示證據結論,status 表示處理進度。null。resolved 必須有修正時間與通過的 Regression Test。Day 23 的三個 Candidate 現在不再只是三段模型回答。
它們成為:
AUTH-01
已確認、已修正、已通過回歸測試。
SECRET-01
已拒絕,保留反證與誤報原因。
RATE-01
等待正式部署與負載證據,尚未指定 Severity。
明天,我們會把這份 Schema 放進 Terminal,建立一行指令就能檢查本機專案、輸出 Artifact,並用 Exit Code 告訴 CI 這次 Review 是否通過。