From 5e7fb9256f1247d2bd35f3b8c844e2b92707f1d1 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:47:06 +0800 Subject: [PATCH 1/2] fix: retain verified staged findings across object budget gaps --- crates/codeguard-cli/src/git_index_safety.rs | 94 ++++++++++++++----- .../tests/git_index_safety_contract.rs | 41 ++++++++ .../specs/verdict-integrity/spec.md | 4 + .../introduce-rust-codeguard-cli/tasks.md | 2 + tests/acceptance/git-index-safety-preview.md | 2 + 5 files changed, 119 insertions(+), 24 deletions(-) diff --git a/crates/codeguard-cli/src/git_index_safety.rs b/crates/codeguard-cli/src/git_index_safety.rs index df69cdb1..3635b666 100644 --- a/crates/codeguard-cli/src/git_index_safety.rs +++ b/crates/codeguard-cli/src/git_index_safety.rs @@ -1,6 +1,6 @@ //! 真实 Git index 路径的有界只读观察;结果不认证完整质量门禁。 -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; @@ -44,10 +44,17 @@ pub struct IndexSafetyObservation { pub objects_verified: bool, pub object_evidence: Vec, pub unresolved_object_paths: Vec, - /// 对象读取或身份核验失败原因;路径违规仍保留。 + /// 对象预算、读取或身份核验未完成原因;已核对的路径及内容发现仍保留。 pub object_verification_reason: Option, } +struct VerifiedIndexObjects { + evidence: Vec, + unresolved_paths: Vec, + content_violations: Vec, + incomplete_reason: Option, +} + /// 独立计算 Git blob 对象身份:`blob <字节长度>\0<内容>`。 #[must_use] pub fn verify_git_blob_oid(content: &[u8], object_format: &str, expected_oid: &str) -> bool { @@ -200,9 +207,13 @@ pub(crate) fn observe_index_safety_with_deadline( } let (object_evidence, unresolved_object_paths, object_verification_reason) = match object_result { - Ok((evidence, unresolved, content_violations)) => { - violations.extend(content_violations); - (evidence, unresolved, None) + Ok(result) => { + violations.extend(result.content_violations); + ( + result.evidence, + result.unresolved_paths, + result.incomplete_reason, + ) } Err(reason) => (Vec::new(), paths.clone(), Some(reason)), }; @@ -228,14 +239,7 @@ fn verify_index_objects( entries: &[GitIndexEntry], format: &str, deadline: Instant, -) -> Result< - ( - Vec, - Vec, - Vec, - ), - String, -> { +) -> Result { let mut requested = BTreeMap::<&str, usize>::new(); let mut unresolved = Vec::new(); for entry in entries { @@ -246,7 +250,12 @@ fn verify_index_objects( } } if requested.is_empty() { - return Ok((Vec::new(), unresolved, Vec::new())); + return Ok(VerifiedIndexObjects { + evidence: Vec::new(), + unresolved_paths: unresolved, + content_violations: Vec::new(), + incomplete_reason: None, + }); } let oid_request = requested .keys() @@ -268,6 +277,8 @@ fn verify_index_objects( return Err("Git 对象大小响应数量或结束符错误".into()); } let mut total = 0_usize; + let mut skipped_oids = BTreeSet::new(); + let mut budget_reason = None; for ((oid, size), line) in requested.iter_mut().zip(lines) { let line = std::str::from_utf8(line).map_err(|_| "Git 对象大小响应非 UTF-8")?; let expected_prefix = format!("{oid} blob "); @@ -277,28 +288,50 @@ fn verify_index_objects( .parse::() .map_err(|_| "Git 对象长度无效")?; if count > 8 * 1024 * 1024 { - return Err("Git 单个对象超出本次验证预算".into()); + skipped_oids.insert(*oid); + budget_reason.get_or_insert_with(|| "Git 单个对象超出本次验证预算".to_owned()); + continue; } - total = total.checked_add(count).ok_or("Git 对象总长度溢出")?; - if total > 128 * 1024 * 1024 { - return Err("Git 对象总长度超出本次验证预算".into()); + let Some(next_total) = total.checked_add(count) else { + skipped_oids.insert(*oid); + budget_reason.get_or_insert_with(|| "Git 对象总长度溢出".to_owned()); + continue; + }; + if next_total > 128 * 1024 * 1024 { + skipped_oids.insert(*oid); + budget_reason.get_or_insert_with(|| "Git 对象总长度超出本次验证预算".to_owned()); + continue; } + total = next_total; *size = count; } let mut verified = BTreeMap::::new(); let mut batch = Vec::<(&str, usize)>::new(); let mut batch_size = 0_usize; + let mut read_failure = None; for (oid, size) in requested { + if skipped_oids.contains(oid) { + continue; + } if !batch.is_empty() && (batch.len() >= 64 || batch_size + size > 8 * 1024 * 1024) { - read_blob_batch(root, tool, index, &batch, format, deadline, &mut verified)?; + if let Err(reason) = + read_blob_batch(root, tool, index, &batch, format, deadline, &mut verified) + { + read_failure = Some(reason); + break; + } batch.clear(); batch_size = 0; } batch.push((oid, size)); batch_size += size; } - if !batch.is_empty() { - read_blob_batch(root, tool, index, &batch, format, deadline, &mut verified)?; + if read_failure.is_none() && !batch.is_empty() { + if let Err(reason) = + read_blob_batch(root, tool, index, &batch, format, deadline, &mut verified) + { + read_failure = Some(reason); + } } let mut evidence = Vec::new(); let mut content_violations = Vec::new(); @@ -306,8 +339,10 @@ fn verify_index_objects( if entry.mode == "160000" { continue; } - let (size, digest, lfs_pointer, private_key) = - verified.get(&entry.oid).ok_or("Git 对象验证结果缺失")?; + let Some((size, digest, lfs_pointer, private_key)) = verified.get(&entry.oid) else { + unresolved.push(entry.path.clone()); + continue; + }; let kind = if entry.mode == "120000" { "symlink_unresolved" } else if *lfs_pointer { @@ -334,7 +369,18 @@ fn verify_index_objects( }); } unresolved.sort(); - Ok((evidence, unresolved, content_violations)) + unresolved.dedup(); + let reason = match (budget_reason, read_failure) { + (Some(_), Some(_)) => Some("Git 对象预算与读取均未完成".into()), + (Some(reason), None) | (None, Some(reason)) => Some(reason), + (None, None) => None, + }; + Ok(VerifiedIndexObjects { + evidence, + unresolved_paths: unresolved, + content_violations, + incomplete_reason: reason, + }) } fn read_blob_batch( diff --git a/crates/codeguard-cli/tests/git_index_safety_contract.rs b/crates/codeguard-cli/tests/git_index_safety_contract.rs index 39fece76..f96b870b 100644 --- a/crates/codeguard-cli/tests/git_index_safety_contract.rs +++ b/crates/codeguard-cli/tests/git_index_safety_contract.rs @@ -288,6 +288,47 @@ fn object_size_limit_does_not_hide_a_staged_path_violation() { assert!(observed.object_verification_reason.is_some()); } +#[test] +fn oversized_staged_blob_does_not_erase_a_verified_private_key_finding() { + let repo = Repo::new(); + fs::create_dir_all(repo.0.join(".codeguard/findings")).unwrap(); + fs::write( + repo.0.join(".codeguard/findings/CG-demo.md"), + synthetic_ed25519_pem(), + ) + .unwrap(); + fs::write(repo.0.join("large.bin"), vec![b'x'; 8 * 1024 * 1024 + 1]).unwrap(); + repo.git(&["add", ".codeguard/findings/CG-demo.md", "large.bin"]); + let observed = observe_index_safety(&repo.0, &git_binary(), None).unwrap(); + assert!(!observed.objects_verified); + assert_eq!(observed.unresolved_object_paths, ["large.bin"]); + assert_eq!(observed.object_evidence.len(), 1); + assert!(observed.object_verification_reason.is_some()); + assert!(observed.violations.iter().any(|item| { + item.path == ".codeguard/findings/CG-demo.md" + && item.rule_id == "repository_policy.unencrypted_openssh_ed25519_private_key" + })); + let output = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "gate", + "pre-commit", + repo.0.to_str().unwrap(), + "--format=json", + ]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(3)); + let report: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(report["object_status"], "failed"); + assert_eq!(report["verified_object_count"], 1); + assert_eq!(report["unresolved_object_count"], 1); + assert_eq!(report["delivery_decision"], "not_evaluated"); + assert!(report["violations"].as_array().unwrap().iter().any(|item| { + item["rule_id"] == "repository_policy.unencrypted_openssh_ed25519_private_key" + })); + assert!(!String::from_utf8_lossy(&output.stdout).contains("BEGIN OPENSSH")); +} + #[test] fn public_gate_preview_reports_staged_violation_without_a_false_allow() { let repo = Repo::new(); diff --git a/openspec/changes/introduce-rust-codeguard-cli/specs/verdict-integrity/spec.md b/openspec/changes/introduce-rust-codeguard-cli/specs/verdict-integrity/spec.md index 1e1ef829..036c9870 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/specs/verdict-integrity/spec.md +++ b/openspec/changes/introduce-rust-codeguard-cli/specs/verdict-integrity/spec.md @@ -39,6 +39,10 @@ Rust 交付普通 allow MUST 同时满足全部适用必需义务完成、覆盖 - **WHEN** 必需规则检出历史文件中的违规 - **THEN** 保留 finding 并按同一策略阻断,基线分类不能改变结果 +#### Scenario: One staged blob exceeds the scan budget after another yields a finding +- **WHEN** 同一 Git index 中的普通 blob 已经核对身份并检出私钥,而另一个暂存对象超过读取预算 +- **THEN** 保留已核对内容的 finding、路径和规则 ID;超预算对象单独列为 unresolved,整体结论仍为 incomplete,不能清空全部发现或签发交付通过 + #### Scenario: Approved false positive is the only blocker - **WHEN** 全部适用义务完整,唯一原始阻断 finding 有当前输入匹配的可信误报白名单 - **THEN** 报告保留原始 finding 和处置引用,交付决策为 allow_with_exceptions diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index ac9e4457..604575b4 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -26,6 +26,7 @@ - [x] 2.2 定义请求/计划/报告/工具锁 JSON schemas 与兼容版本策略,提供完整正反例;验收:未知 major/非法枚举不按 PASS 消费,运行报告区分项目检查器的 `configured/missing/invalid/unknown` 与本次原生结果。证据:相邻 `codeguard-cli/schemas/run-report.schema.json` 的 1.2 协议、`crates/codeguard-cli/src/run_report.rs` 与 `tests/run_report_contract.rs` 的正反例;1.0 历史报告仍可读取,1.1 起区分配置和运行,1.2 起 finding 要求定位且检查器状态按构建根区分。此项只完成协议,不代表项目配置探测或宿主对话接线已完成。 公共协议输入加固:Rust 检查请求和 RunReport 现从原始字节递归拒绝重复 JSON 键,并复用 16 MiB 输入上限;`delivery_gate`/`decision` 和嵌套 `jobs` 重复键反例先失败后通过。此校验只保证结构消费,不验证报告来源或正式门禁。 - [ ] 2.3 实现 findings 与 completion 双维聚合及新退出码;验收:混合违规/缺工具返回 3 并保留全部发现,取消/内部异常优先级正确。 + Git 暂存增量:`gate pre-commit` 的一个对象超出 8 MiB 预算时,已核对普通 blob 的私钥 finding 不再被整体丢弃;公开报告同时保留 1 项发现、1 项 unresolved,仍固定退出 3/交付未评估。真实 Git 混合样本见 `tests/acceptance/git-index-safety-preview.md`。这不是跨命令完整双维聚合,2.3 不勾选。 Python CVE 部分结果增量:`check all` 对完整但原生异常退出或输出超限的 pip-audit 报告,保留已归属 advisory;执行节点与类别候选均为 `native_incomplete`,未解决条件保留 `python_cve_task_incomplete`。旧汇总层仅按 `native_report_valid=true` 将部分报告误记成功,反例先 RED 后修正;两份实际总报告通过 schema。完整 findings/completion 跨语言、取消与内部异常优先级仍缺,2.3 不勾选,见相邻 `codeguard-cli/tests/acceptance/python-cve-partial-native.md`。 内部故障反馈进展:`check all/java` 的任务图若返回内部故障,现以退出 4 输出结构化 `check_aborted`,保留兄弟任务已取得的原生局部结果、发现及每项任务状态;交付为 incomplete/not_evaluated。同轮同时有取消时优先退出 130,仍保留故障任务 ID 与原生发现。它不是完整 RunReport,调度器自身失败和跨入口聚合尚未完成,2.3 仍不勾选。 取消优先级进展:`check all/java` 的 `check_feedback` 升至 0.16,实际 SIGINT 并行回归证明一项 Ruff 原生任务取消后退出 130,同时保留另一项已完成的 Rust Clippy finding;状态明确为 cancelled,交付仍 incomplete/not_evaluated。`lint python` 对话反馈也升至 0.11,真实 SIGINT 原生探测退出 130,保留局部反馈与子孙清理证据。其它入口、调度器故障与完整义务聚合仍缺,2.3 不勾选。 @@ -74,6 +75,7 @@ Java 扩展:`check_feedback` 0.7 增加 `java.p3c` 独立节点,复用相同截止时间和取消标志,缺项目配置或工具前提仍保留文件级原因。原生 Maven 探针使用私有临时工作区;完整规则集、跨模块构建资源锁及工作区级义务仍缺,3.4 不勾选。 并发私有工作区补强:共享 `DoctorScratch` 在内部标签后追加进程 ID 与进程内单调序号,防止同一时钟刻度的多个任务争抢同名 0700 目录;目录仍独占创建、不复用残留。并发同标签 RED→GREEN、Python CVE 多根 20 次及整组 15 次重复运行见[局部验收](../../../tests/acceptance/private-scratch-concurrency.md)。跨进程资源锁与完整 CheckPlan 仍缺,3.4 不勾选。 - [ ] 3.5 实现工作树/index/ref 快照与原始字节校验;验收:SHA-1/SHA-256、坏批响应、特殊文件、symlink/gitlink/LFS 均明确处理。进行中:相邻 Rust CLI 已按 NUL 协议读取真实 index、前后复核列表,并对有界普通 blob 独立核验 SHA-1/SHA-256 Git OID、记录脱敏字节摘要;symlink/gitlink/LFS、超预算和对象读取失败明确 unresolved,见 `codeguard-cli/tests/acceptance/git-index-safety-preview.md`。工作树/ref 快照、坏批响应注入测试、特殊类型完整语义及 Git 工具身份未完成。 + 局部保留进展:单对象/总预算超限不再清空其它已核对 blob 的证据;局部原生 batch 故障也保留先前逐对象核对的结果,并将余项列为 unresolved。真实超预算混合样本通过;坏批响应注入及跨平台实测仍缺,3.5 不勾选。 - [ ] 3.6 实现 GIT_INDEX_FILE、初始提交、worktree、多 ref/non-HEAD/删除 ref push 输入;验收:真实临时 Git 仓检查准确且 index 不变。进行中:相邻 Rust 的真实 index 路径安全预览覆盖初始仓库、替代 `GIT_INDEX_FILE` 与默认 index 不变;worktree 和 pre-push 多 ref/删除 ref 尚未实现,不勾选。 - [ ] 3.7 实现执行前后内容身份复核和源码副作用检测;验收:并发编辑或检查器改源码导致 incomplete。 进行中:相邻 Rust runtime 新增显式相对路径的有界 `SourceSnapshot`,拒绝静态可见的路径越界、重复、符号链接及预算超限;私有副本须写入新目录,运行后复核原件与副本字节。JDK Javadoc 单文件入口已使用此契约,真实 JDK 21 与链接/修改反例通过,见 `codeguard-cli/tests/acceptance/source-snapshot-boundary.md`。调用方源集完整性、并发目录替换的内核级隔离、其它 adapter 接线和项目级 Maven 副作用仍缺,3.7 不勾选。 diff --git a/tests/acceptance/git-index-safety-preview.md b/tests/acceptance/git-index-safety-preview.md index d84e8804..1ee1c327 100644 --- a/tests/acceptance/git-index-safety-preview.md +++ b/tests/acceptance/git-index-safety-preview.md @@ -9,3 +9,5 @@ Rust CLI 的 `observe_index_safety` 通过受控原生 Git 进程读取仓库根 目标测试先因 Rust 模块缺失失败;随后验证暂存 `.env`、未暂存 `.pem`、替代 index、默认 index 不变、真实 SHA-1/SHA-256 仓库、暂存后工作树改动、symlink/LFS、超预算对象仍保留路径违规、坏记录/冲突、非 Git 目录、公开 CLI JSON/退出码及坏参数,并校验公开预览 schema 不含 allow 值。`git_index_safety_contract` 共 13 项通过,`repository_path_safety_contract` 共 3 项通过。真实 Git Hook、pre-push 多 ref、CI 与三宿主接线不在此切片内,OpenSpec 3.5/3.6/4.6/11.5/12.12 保持未完成。 2026-09-29 内容增量采用红→绿:先增加在 `.codeguard/findings/CG-demo.md` 暂存结构化私钥、随后把工作树同一路径改为安全文本的反例,测试因无内容违规失败;接入 Rust 纯结构解析和已核对 Git blob 的路径映射后通过。Core 目标测试 2 项、真实 Git/CLI 目标测试 15 项通过;CRLF、空壳 PEM、无效 base64、截断、损坏填充和未暂存密钥均有反例。本机 `ssh-keygen` 生成真实 Ed25519 私钥的首次验收暴露合法“零填充”格式漏报;修正后同一真实暂存样本在普通 `notes.md` 路径命中稳定规则,CLI 仍为 `not_evaluated`。`cargo fmt --all --check` 与 `git diff --check` 通过;全工作区回归在磁盘可用空间降至 801 MiB 时主动中断,未声称通过,随后仅清理本仓可重建的 Cargo dev 构建产物,磁盘恢复至约 20 GiB。首轮 PR CI 因 crate 依赖白名单缺少 base64 失败;修正并增加限定依赖的反例后,[第二轮 CI](https://github.com/full-stack-plugins/codeguard/actions/runs/36517569232) 通过完整 workspace 构建、Linux WASM worker 测试与全量测试。它仍不证明正式门禁已完成。 + +后续混合证据反例:同一真实 Git index 暂存 `.codeguard/findings/CG-demo.md` 中的结构化私钥和超过 8 MiB 的 `large.bin`。改动前对象大小预检整体报错,两个路径都列为 unresolved,私钥 finding 消失;改动后单独跳过超预算 OID,其余对象继续核对,保留一条私钥 finding、一条超预算 unresolved,CLI JSON 的 `verified_object_count=1`、`unresolved_object_count=1`、`delivery_decision=not_evaluated` 且退出 3。目标 Git/CLI 契约 16 项通过;完整工作区与 Linux CI 仍待本次提交验证。此修正仍不是完整内容扫描、原子 Git 快照或正式提交门禁。 From 86a9613cf83b39b23c57e623b0f03115a1820ad1 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:50:19 +0800 Subject: [PATCH 2/2] test: retain verified objects after a corrupt later Git batch --- .../tests/git_index_safety_contract.rs | 33 +++++++++++++++++++ .../introduce-rust-codeguard-cli/tasks.md | 2 +- tests/acceptance/git-index-safety-preview.md | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/codeguard-cli/tests/git_index_safety_contract.rs b/crates/codeguard-cli/tests/git_index_safety_contract.rs index f96b870b..4fd2f286 100644 --- a/crates/codeguard-cli/tests/git_index_safety_contract.rs +++ b/crates/codeguard-cli/tests/git_index_safety_contract.rs @@ -329,6 +329,39 @@ fn oversized_staged_blob_does_not_erase_a_verified_private_key_finding() { assert!(!String::from_utf8_lossy(&output.stdout).contains("BEGIN OPENSSH")); } +#[test] +fn later_corrupt_git_batch_keeps_earlier_oid_verified_objects_incomplete() { + use std::os::unix::fs::PermissionsExt; + + let repo = Repo::new(); + for index in 0..65 { + fs::write( + repo.0.join(format!("file-{index}.txt")), + format!("value {index}\n"), + ) + .unwrap(); + } + repo.git(&["add", "."]); + let marker = repo.0.join("first-batch-read"); + let wrapper = repo.0.join("git-wrapper"); + let script = format!( + "#!/bin/sh\nif [ \"$1\" = cat-file ] && [ \"$2\" = --batch ]; then\n if [ -e '{}' ]; then printf 'corrupt\\n'; exit 0; fi\n : > '{}'\nfi\nexec '{}' \"$@\"\n", + marker.display(), + marker.display(), + git_binary().display() + ); + fs::write(&wrapper, script).unwrap(); + fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o700)).unwrap(); + + let observed = observe_index_safety(&repo.0, &wrapper, None).unwrap(); + assert_eq!(observed.entries.len(), 65); + assert_eq!(observed.object_evidence.len(), 64); + assert_eq!(observed.unresolved_object_paths.len(), 1); + assert!(observed.object_verification_reason.is_some()); + assert!(!observed.objects_verified); + assert!(observed.violations.is_empty()); +} + #[test] fn public_gate_preview_reports_staged_violation_without_a_false_allow() { let repo = Repo::new(); diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index 604575b4..271b5a85 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -75,7 +75,7 @@ Java 扩展:`check_feedback` 0.7 增加 `java.p3c` 独立节点,复用相同截止时间和取消标志,缺项目配置或工具前提仍保留文件级原因。原生 Maven 探针使用私有临时工作区;完整规则集、跨模块构建资源锁及工作区级义务仍缺,3.4 不勾选。 并发私有工作区补强:共享 `DoctorScratch` 在内部标签后追加进程 ID 与进程内单调序号,防止同一时钟刻度的多个任务争抢同名 0700 目录;目录仍独占创建、不复用残留。并发同标签 RED→GREEN、Python CVE 多根 20 次及整组 15 次重复运行见[局部验收](../../../tests/acceptance/private-scratch-concurrency.md)。跨进程资源锁与完整 CheckPlan 仍缺,3.4 不勾选。 - [ ] 3.5 实现工作树/index/ref 快照与原始字节校验;验收:SHA-1/SHA-256、坏批响应、特殊文件、symlink/gitlink/LFS 均明确处理。进行中:相邻 Rust CLI 已按 NUL 协议读取真实 index、前后复核列表,并对有界普通 blob 独立核验 SHA-1/SHA-256 Git OID、记录脱敏字节摘要;symlink/gitlink/LFS、超预算和对象读取失败明确 unresolved,见 `codeguard-cli/tests/acceptance/git-index-safety-preview.md`。工作树/ref 快照、坏批响应注入测试、特殊类型完整语义及 Git 工具身份未完成。 - 局部保留进展:单对象/总预算超限不再清空其它已核对 blob 的证据;局部原生 batch 故障也保留先前逐对象核对的结果,并将余项列为 unresolved。真实超预算混合样本通过;坏批响应注入及跨平台实测仍缺,3.5 不勾选。 + 局部保留进展:单对象/总预算超限不再清空其它已核对 blob 的证据;局部原生 batch 故障也保留先前逐对象核对的结果,并将余项列为 unresolved。真实超预算混合样本通过;受控 Git 第二批损坏响应反例证明前 64 个 OID 证据保留、第 65 个未完成。其它坏批响应变体、工作树/ref 快照及跨平台实测仍缺,3.5 不勾选。 - [ ] 3.6 实现 GIT_INDEX_FILE、初始提交、worktree、多 ref/non-HEAD/删除 ref push 输入;验收:真实临时 Git 仓检查准确且 index 不变。进行中:相邻 Rust 的真实 index 路径安全预览覆盖初始仓库、替代 `GIT_INDEX_FILE` 与默认 index 不变;worktree 和 pre-push 多 ref/删除 ref 尚未实现,不勾选。 - [ ] 3.7 实现执行前后内容身份复核和源码副作用检测;验收:并发编辑或检查器改源码导致 incomplete。 进行中:相邻 Rust runtime 新增显式相对路径的有界 `SourceSnapshot`,拒绝静态可见的路径越界、重复、符号链接及预算超限;私有副本须写入新目录,运行后复核原件与副本字节。JDK Javadoc 单文件入口已使用此契约,真实 JDK 21 与链接/修改反例通过,见 `codeguard-cli/tests/acceptance/source-snapshot-boundary.md`。调用方源集完整性、并发目录替换的内核级隔离、其它 adapter 接线和项目级 Maven 副作用仍缺,3.7 不勾选。 diff --git a/tests/acceptance/git-index-safety-preview.md b/tests/acceptance/git-index-safety-preview.md index 1ee1c327..d982e78d 100644 --- a/tests/acceptance/git-index-safety-preview.md +++ b/tests/acceptance/git-index-safety-preview.md @@ -11,3 +11,5 @@ Rust CLI 的 `observe_index_safety` 通过受控原生 Git 进程读取仓库根 2026-09-29 内容增量采用红→绿:先增加在 `.codeguard/findings/CG-demo.md` 暂存结构化私钥、随后把工作树同一路径改为安全文本的反例,测试因无内容违规失败;接入 Rust 纯结构解析和已核对 Git blob 的路径映射后通过。Core 目标测试 2 项、真实 Git/CLI 目标测试 15 项通过;CRLF、空壳 PEM、无效 base64、截断、损坏填充和未暂存密钥均有反例。本机 `ssh-keygen` 生成真实 Ed25519 私钥的首次验收暴露合法“零填充”格式漏报;修正后同一真实暂存样本在普通 `notes.md` 路径命中稳定规则,CLI 仍为 `not_evaluated`。`cargo fmt --all --check` 与 `git diff --check` 通过;全工作区回归在磁盘可用空间降至 801 MiB 时主动中断,未声称通过,随后仅清理本仓可重建的 Cargo dev 构建产物,磁盘恢复至约 20 GiB。首轮 PR CI 因 crate 依赖白名单缺少 base64 失败;修正并增加限定依赖的反例后,[第二轮 CI](https://github.com/full-stack-plugins/codeguard/actions/runs/36517569232) 通过完整 workspace 构建、Linux WASM worker 测试与全量测试。它仍不证明正式门禁已完成。 后续混合证据反例:同一真实 Git index 暂存 `.codeguard/findings/CG-demo.md` 中的结构化私钥和超过 8 MiB 的 `large.bin`。改动前对象大小预检整体报错,两个路径都列为 unresolved,私钥 finding 消失;改动后单独跳过超预算 OID,其余对象继续核对,保留一条私钥 finding、一条超预算 unresolved,CLI JSON 的 `verified_object_count=1`、`unresolved_object_count=1`、`delivery_decision=not_evaluated` 且退出 3。目标 Git/CLI 契约 16 项通过;完整工作区与 Linux CI 仍待本次提交验证。此修正仍不是完整内容扫描、原子 Git 快照或正式提交门禁。 + +补充坏批响应反例:受控 Git 替身将 65 个不同 OID 的首批 64 个返回真实原生命令输出,第二批返回损坏对象头。观察保留前 64 个逐对象核对证据,仅把第 65 个路径列为 unresolved,标记对象读取未完成且 `objects_verified=false`;没有从此前局部成功推导完整结果。目标 Git/CLI 契约增至 17 项通过,其余异常响应和实际平台边界仍由后续验收覆盖。