Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The authored evaluation benchmark is located in `evaluation/questions.json`, wit

- **No In-Binary Hybrid or Vector Search**: Vector embeddings and hybrid search algorithms exist only in the offline Python script `evaluation/evaluate_hybrid.py`. They are **not** integrated into the Rust product binary, library, or HTTP API.
- **Heuristic Citation Screening, Not Answer Verification**: The CLI screens some plain `path:line` tokens against retrieved text. This is not a complete citation parser and does not prove that an answer is supported by its sources. The lexical evaluator reports retrieval Recall@5 and MRR only, not answer or citation accuracy.
- **File Access Policy**: Initial scans and incremental updates exclude hidden path components, common build/dependency directories, `.pem`/`.key` files, and named SSH private keys. Incremental updates reject absolute/parent paths and symlink components. This name-based policy is not a secret scanner and does not defend against a hostile concurrent filesystem mutation.
- **File Access Policy**: Initial scans and incremental updates exclude hidden path components, common build/dependency directories, `.pem`/`.key` files, and named SSH private keys. Incremental updates reject absolute/parent paths and symlink components. Sensitive-name matching is ASCII case-insensitive, so an alias like `CREDENTIALS.JSON` cannot bypass the policy on a case-insensitive filesystem. This name-based policy is not a secret scanner and does not defend against a hostile concurrent filesystem mutation.
- **Working-Tree Prototype**: Files are read from the working tree, not an immutable Git snapshot. A `-dirty` label is diagnostic, not a reproducible snapshot identifier. Commit-consistent indexing and complete rename handling remain unfinished.
- **No Background Watcher**: Background filesystem events are not monitored; synchronization is triggered explicitly via CLI or HTTP `/reload`.
- **No Production Hardening**: Authentication, TLS, rate limiting, and multi-tenant isolation remain out of scope for this local development prototype.
Expand Down
63 changes: 63 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ fn is_indexable(path: &Path) -> bool {
/// Single source of truth for file names that must never be indexed.
/// Shared by the full walk and incremental updates so a scan and a rebase agree.
fn is_sensitive_file_name(name: &str) -> bool {
let name = name.to_ascii_lowercase();
(name.starts_with('.') && name != ".github" && name != ".gitignore")
|| name.ends_with(".pem")
|| name.ends_with(".key")
Expand Down Expand Up @@ -463,4 +464,66 @@ mod tests {
.unwrap();
assert!(index.search("legacycanary", 5).is_empty());
}

#[test]
fn sensitive_names_match_case_insensitively() {
for name in [
"CREDENTIALS.JSON",
"Service-Account.json",
"CLIENT.P12",
"CLIENT.PFX",
"RELEASE.KEYSTORE",
"ID_RSA",
"ID_ED25519",
"SERVER.PEM",
"SERVER.KEY",
".ENV",
".Env.local",
] {
assert!(is_sensitive_file_name(name), "{name} must be sensitive");
}
for name in ["credentials.rs", "service.rs", "keep.p12.bak"] {
assert!(
!is_sensitive_file_name(name),
"{name} must not be sensitive"
);
}
}

#[test]
fn full_scan_and_update_agree_on_case_variant_sensitive_names() {
let root = fixture();
fs::create_dir_all(root.join("sub")).unwrap();
let sensitive = [
"CREDENTIALS.JSON",
"Service-Account.json",
"CLIENT.P12",
"sub/RELEASE.PFX",
"sub/KEYSTORE.KEYSTORE",
"ID_RSA",
];
for name in sensitive {
fs::write(root.join(name), "casecanary").unwrap();
}

let mut index = Index::build(&root).unwrap();
assert!(index.search("casecanary", 20).is_empty());
for name in sensitive {
index.update_file(&root, Path::new(name)).unwrap();
}
assert!(index.search("casecanary", 20).is_empty());
}

#[test]
fn update_file_case_alias_cannot_bypass_sensitive_policy() {
let root = fixture();
fs::write(root.join("credentials.json"), "aliascanary").unwrap();
let mut index = Index::build(&root).unwrap();
assert!(index.search("aliascanary", 5).is_empty());

index
.update_file(&root, Path::new("CREDENTIALS.JSON"))
.unwrap();
assert!(index.search("aliascanary", 5).is_empty());
}
}
Loading