Skip to content

Commit d46eb4c

Browse files
authored
Merge pull request #5 from full-stack-plugins/feat/rust-hook-prompt-guidance
feat: detect staged OpenSSH Ed25519 keys in Rust safety preview
2 parents 7bd977e + 9ee236d commit d46eb4c

27 files changed

Lines changed: 410 additions & 39 deletions

‎Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ serde_json = "1"
1919
sha2 = "0.10.9"
2020
sha1 = "=0.10.7"
2121
ring = "=0.17.14"
22+
base64 = "=0.22.1"

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Supply the original checker's required tool/configuration options to `task verif
169169
| `task claim / heartbeat / release`, `task attempt start / finish` | Local ownership and attempt history | Unix local coordination, not distributed locking |
170170
| `task verify` | Repeat selected original checkers | Formal closure/reopening pending |
171171
| `rules whitelist list / explain / propose` | Inspect/propose false-positive dispositions and corrections | No public approval or activation |
172-
| `gate pre-commit` | Git-index path-safety observation | Not a full content/security gate |
172+
| `gate pre-commit` | Git-index path/object observation and narrow OpenSSH Ed25519 key detection | Incomplete preview; not a full content/security gate |
173173

174174
For Python edit feedback, run `codeguard lint python . --file src/changed.py --format json`. `--file` is repeatable with a limit of eight distinct paths, 512 bytes per path and 2 KiB combined. The response declares `scan_scope=selected_files` and observes only those files; it is not imported as a full workspace scan and cannot approve delivery. Without `--file`, the existing command still scans discovered Python files and synchronizes its local report.
175175

‎README.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ codeguard task verify "$TASK_ID" . --format json
168168
| `task claim / heartbeat / release`、`task attempt start / finish` | 本地占用与尝试历史 | Unix 本地协作,不保证分布式锁 |
169169
| `task verify` | 重跑部分原检查器 | 正式关闭与重开待完成 |
170170
| `rules whitelist list / explain / propose` | 查询或提出误报处置与纠错 | 无公开批准或生效入口 |
171-
| `gate pre-commit` | Git index 路径安全观察 | 尚非完整内容或安全门禁 |
171+
| `gate pre-commit` | Git index 路径、对象及未加密 OpenSSH Ed25519 私钥局部观察 | 仍是不完整预览,尚非完整内容或安全门禁 |
172172

173173
Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py --format json`;`--file` 可重复,最多 8 个不同路径,单路径 512 字节、总计 2 KiB。报告标明 `scan_scope=selected_files`,只观察选中文件;局部反馈不导入完整工作台,也不代表全项目或交付通过。原有不带 `--file` 的命令仍扫描发现到的 Python 文件并同步局部报告。
174174

‎crates/codeguard-adapters/src/eslint_finding_identity.rs‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,7 @@ pub fn project_eslint_findings(
103103
}
104104

105105
/// 复核脱敏任务输入的稳定投影;消息摘要只验证形状,不自证原生来源。
106-
pub fn validate_records(
107-
relative: &str,
108-
absolute: &str,
109-
source: &[u8],
110-
records: &[Value],
111-
) -> bool {
106+
pub fn validate_records(relative: &str, absolute: &str, source: &[u8], records: &[Value]) -> bool {
112107
let mut diagnostics = Vec::new();
113108
for record in records {
114109
let native = &record["native"];

‎crates/codeguard-cli/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ sha1.workspace = true
2323
http = "=1.5.0"
2424

2525
[dev-dependencies]
26+
base64.workspace = true
2627
ring.workspace = true
2728
tokio = { version="=1.53.1", features=["rt","time","net"] }

‎crates/codeguard-cli/src/eslint_workbench.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! ESLint 局部原生观察到既有修复队列的连接;没有审批或关闭权威。
2-
use codeguard_adapters::project_eslint_findings;
32
use crate::{
43
eslint_lint_arguments::EslintLintArguments,
54
eslint_probe_request::EslintProbeRequest,
65
eslint_probe_result::EslintProbeResult,
76
work_sync::{save_local_report, sync_local_workspace},
87
workspace_refresh::read_workspace_baseline,
98
};
9+
use codeguard_adapters::project_eslint_findings;
1010
use codeguard_runtime::read_bounded_regular_file;
1111
use serde_json::{Value, json};
1212
use sha2::{Digest, Sha256};

‎crates/codeguard-cli/src/git_index_safety.rs‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use std::path::{Path, PathBuf};
77
use std::sync::atomic::AtomicBool;
88
use std::time::{Duration, Instant};
99

10-
use codeguard_core::{RepositoryPathViolation, check_repository_paths};
10+
use codeguard_core::{
11+
RepositoryPathViolation, check_repository_paths, has_unencrypted_openssh_ed25519_private_key,
12+
};
1113
use codeguard_runtime::{ProcessSpec, Termination, run_process};
1214
use sha1::Sha1;
1315
use sha2::{Digest, Sha256};
@@ -183,7 +185,7 @@ pub(crate) fn observe_index_safety_with_deadline(
183185
let first = git(&root, git_tool, index.as_deref(), &args, deadline, None)?;
184186
let entries = parse_index_listing(&first, format)?;
185187
let paths: Vec<String> = entries.iter().map(|entry| entry.path.clone()).collect();
186-
let violations = check_repository_paths(&paths)?;
188+
let mut violations = check_repository_paths(&paths)?;
187189
let object_result = verify_index_objects(
188190
&root,
189191
git_tool,
@@ -198,7 +200,10 @@ pub(crate) fn observe_index_safety_with_deadline(
198200
}
199201
let (object_evidence, unresolved_object_paths, object_verification_reason) = match object_result
200202
{
201-
Ok((evidence, unresolved)) => (evidence, unresolved, None),
203+
Ok((evidence, unresolved, content_violations)) => {
204+
violations.extend(content_violations);
205+
(evidence, unresolved, None)
206+
}
202207
Err(reason) => (Vec::new(), paths.clone(), Some(reason)),
203208
};
204209
let objects_verified = object_verification_reason.is_none()
@@ -223,7 +228,14 @@ fn verify_index_objects(
223228
entries: &[GitIndexEntry],
224229
format: &str,
225230
deadline: Instant,
226-
) -> Result<(Vec<IndexObjectEvidence>, Vec<String>), String> {
231+
) -> Result<
232+
(
233+
Vec<IndexObjectEvidence>,
234+
Vec<String>,
235+
Vec<RepositoryPathViolation>,
236+
),
237+
String,
238+
> {
227239
let mut requested = BTreeMap::<&str, usize>::new();
228240
let mut unresolved = Vec::new();
229241
for entry in entries {
@@ -234,7 +246,7 @@ fn verify_index_objects(
234246
}
235247
}
236248
if requested.is_empty() {
237-
return Ok((Vec::new(), unresolved));
249+
return Ok((Vec::new(), unresolved, Vec::new()));
238250
}
239251
let oid_request = requested
240252
.keys()
@@ -273,7 +285,7 @@ fn verify_index_objects(
273285
}
274286
*size = count;
275287
}
276-
let mut verified = BTreeMap::<String, (usize, String, bool)>::new();
288+
let mut verified = BTreeMap::<String, (usize, String, bool, bool)>::new();
277289
let mut batch = Vec::<(&str, usize)>::new();
278290
let mut batch_size = 0_usize;
279291
for (oid, size) in requested {
@@ -289,11 +301,13 @@ fn verify_index_objects(
289301
read_blob_batch(root, tool, index, &batch, format, deadline, &mut verified)?;
290302
}
291303
let mut evidence = Vec::new();
304+
let mut content_violations = Vec::new();
292305
for entry in entries {
293306
if entry.mode == "160000" {
294307
continue;
295308
}
296-
let (size, digest, lfs_pointer) = verified.get(&entry.oid).ok_or("Git 对象验证结果缺失")?;
309+
let (size, digest, lfs_pointer, private_key) =
310+
verified.get(&entry.oid).ok_or("Git 对象验证结果缺失")?;
297311
let kind = if entry.mode == "120000" {
298312
"symlink_unresolved"
299313
} else if *lfs_pointer {
@@ -304,6 +318,13 @@ fn verify_index_objects(
304318
if kind != "regular" {
305319
unresolved.push(entry.path.clone());
306320
}
321+
// 仅对已核对 OID 的普通 blob 记录精确结构命中;不回显私钥字节。
322+
if kind == "regular" && *private_key {
323+
content_violations.push(RepositoryPathViolation {
324+
path: entry.path.clone(),
325+
rule_id: "repository_policy.unencrypted_openssh_ed25519_private_key".into(),
326+
});
327+
}
307328
evidence.push(IndexObjectEvidence {
308329
path: entry.path.clone(),
309330
oid: entry.oid.clone(),
@@ -313,7 +334,7 @@ fn verify_index_objects(
313334
});
314335
}
315336
unresolved.sort();
316-
Ok((evidence, unresolved))
337+
Ok((evidence, unresolved, content_violations))
317338
}
318339

319340
fn read_blob_batch(
@@ -323,7 +344,7 @@ fn read_blob_batch(
323344
batch: &[(&str, usize)],
324345
format: &str,
325346
deadline: Instant,
326-
verified: &mut BTreeMap<String, (usize, String, bool)>,
347+
verified: &mut BTreeMap<String, (usize, String, bool, bool)>,
327348
) -> Result<(), String> {
328349
let input = batch
329350
.iter()
@@ -364,6 +385,7 @@ fn read_blob_batch(
364385
*size,
365386
format!("{:x}", Sha256::digest(content)),
366387
content.starts_with(b"version https://git-lfs.github.com/spec/v1"),
388+
has_unencrypted_openssh_ed25519_private_key(content),
367389
),
368390
);
369391
cursor = content_end + 1;

‎crates/codeguard-cli/src/git_index_safety_command.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub fn run(args: &[String]) -> ExitCode {
5656
incomplete_reasons.push("index_objects_unresolved");
5757
}
5858
json!({
59-
"schema_version":"0.2.0",
59+
"schema_version":"0.3.0",
6060
"report_type":"git_index_safety_preview",
6161
"operation":"gate_pre_commit",
6262
"command_status":"incomplete",
@@ -77,7 +77,7 @@ pub fn run(args: &[String]) -> ExitCode {
7777
})
7878
}
7979
Err(_) => json!({
80-
"schema_version":"0.2.0",
80+
"schema_version":"0.3.0",
8181
"report_type":"git_index_safety_preview",
8282
"operation":"gate_pre_commit",
8383
"command_status":"incomplete",

‎crates/codeguard-cli/src/go_lint_command.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
33
use crate::check_budget::parse_check_timeout;
44
use crate::discovery::discover;
5-
use codeguard_adapters::go_finding_record;
65
use crate::work_sync::{save_local_report, sync_local_workspace};
76
use crate::workspace_refresh::read_workspace_baseline;
7+
use codeguard_adapters::go_finding_record;
88
use codeguard_adapters::{GoVetParseState, legacy_registry, parse_go_vet_json};
99
use codeguard_runtime::{
1010
NativeObservation, ProcessSpec, Termination, read_bounded_regular_file, run_process,

0 commit comments

Comments
 (0)