git-forensic
Point it at a .git, get back severity-graded Git anomalies — backdated commits, rewritten history, unsigned commits in a signed history, and resurrectable dropped objects as forensicnomicon::report::Findings.
[]
= "0.1" # pulls in git-core
use ;
use GitRepo;
use Path;
let repo = open?;
let head = repo.head?;
for anomaly in audit_repo?
# Ok::
audit_repo walks commits from a starting hash and grades what it finds. Damaged or unreadable objects surface as typed errors, never a panic.
The anomaly codes
Each anomaly is an observation ("consistent with …"); the examiner draws the conclusions. Codes are a stable, published contract.
| Code | Severity | What it observes |
|---|---|---|
GIT-COMMIT-TIME-INVERSION |
Medium | A commit whose committer timestamp precedes its author timestamp — consistent with timestamp backdating (benign cause: cross-machine clock skew) |
GIT-HISTORY-REWRITE |
Medium | A reflog entry whose operation rewrote history (reset / rebase / amend / filter-branch / forced update) — the prior tip remains resurrectable |
GIT-UNSIGNED-IN-SIGNED-HISTORY |
Medium | An unsigned commit within a predominantly-signed history — consistent with a commit injected outside the prevailing signing policy (benign cause: a forgotten signature) |
GIT-UNREACHABLE-OBJECT |
Medium / Low | An object reachable from no ref — residue of deleted or rewritten history, resurrectable until gc (a dropped commit grades Medium; a loose blob/tree grades Low) |
GIT-COMMIT-TIME-INVERSION comes from audit_commit / audit_repo; GIT-HISTORY-REWRITE from audit_reflog / audit_reflog_entries (classify_rewrite is the pure decision core); GIT-UNSIGNED-IN-SIGNED-HISTORY from audit_signatures / audit_signatures_repo; GIT-UNREACHABLE-OBJECT from audit_unreachable. Each anomaly emits a graded report::Finding via to_finding(source).
The attribution timeline
Beyond the graded anomalies, the attribution module flattens a set of commits into the who-did-what-when backbone an examiner narrates on:
attribution_timeline(commits)→ a time-ordered stream of author/committer identity events, timezone offset retained (it can corroborate or contradict a claimed location).distinct_identities(commits)→ the distinct(name, email)identities, in first-seen order.attribution_repo(repo, from)→ the same, walked from a repository tip.
The two-crate split
This crate is the analyzer; the reader is git-core (loose + packfile objects, refs, commits, trees, reflogs, and GitRepo navigation over a .git directory). The split mirrors ntfs-core/ntfs-forensic. Together they feed issen for cross-artifact correlation.
Trust, but verify
Built for untrusted object stores from potentially compromised systems: #![forbid(unsafe_code)]; panic-free on crafted input (the workspace denies clippy::unwrap_used / expect_used in production code); git-core is fuzzed with four cargo-fuzz targets and exercised against real .git directories with packfile delta resolution cross-checked against git itself.
Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd