Skip to main content

rto_exec/
lib.rs

1//! The analyzer execution seam: one contract, interchangeable backends.
2//!
3//! Running an external analyzer (`cargo-audit`, `semgrep`, successors) can happen
4//! in CI, on a developer's machine, or — later — locally inside a sandbox. This
5//! crate exists so those stop being competing architectures: every backend
6//! implements one [`AnalyzerRunner`] trait, takes one [`AnalysisRequest`], and
7//! returns one [`AnalysisResponse`] of normalized findings plus run evidence. A
8//! caller never learns which backend produced a result, so adding the sandboxed
9//! and subprocess backends later changes no call site.
10//!
11//! Today there is exactly one implementation, [`IngestRunner`], which consumes a
12//! normalized report produced elsewhere. It is the zero-install default, not a
13//! fallback: it needs no container runtime and adds no isolation surface, and
14//! what it produces is byte-for-byte the shape a sandboxed run will produce.
15//!
16//! # What this crate does not do
17//!
18//! It does not decide how results are *stored*. Persistence lives in `rto-graph`,
19//! which files findings in their own tables — never `nodes`/`edges`, never a
20//! provenance class, never in the exported graph artifact (ADR-0012). Nothing
21//! here can move the published `GraphArtifact` by a byte, and that is checked by
22//! test rather than assumed.
23//!
24//! No analyzer is implemented here, and no sandbox dependency is pulled in; the
25//! backends arrive behind their own features (ADR-0014).
26//!
27//! @rto:0014
28//! @rto:0012
29//!
30//! # Example
31//!
32//! ```
33//! use rto_exec::{AnalysisRequest, AnalyzerRunner, Consent, IngestRunner, Worktree};
34//! use rto_graph::SourceIdentity;
35//!
36//! let report = br#"{
37//!   "schema": "roteiro.findings/v1",
38//!   "analyzer": "cargo-audit",
39//!   "analyzer_version": "0.21.0",
40//!   "started_at": "2026-08-15T09:00:00Z",
41//!   "ended_at": "2026-08-15T09:00:04Z",
42//!   "exit_status": 1,
43//!   "findings": [{
44//!     "identity": ["RUSTSEC-2024-0001", "openssl", "0.10.5", "lock123"],
45//!     "rule": "RUSTSEC-2024-0001",
46//!     "severity": "high",
47//!     "title": "openssl is vulnerable",
48//!     "message": "upgrade to 0.10.66"
49//!   }]
50//! }"#;
51//!
52//! let request = AnalysisRequest {
53//!     analyzer: "cargo-audit".to_owned(),
54//!     worktree: Worktree::read_only("/repo".as_ref()).expect("worktree"),
55//!     network: rto_graph::NetworkPolicy::Deny,
56//!     consent: Consent::Granted,
57//!     source: SourceIdentity::default(),
58//! };
59//! let response = IngestRunner::new(report.to_vec()).run(&request).expect("ingest");
60//! assert_eq!(response.findings.len(), 1);
61//! assert_eq!(response.run.isolation, rto_graph::Isolation::Ingested);
62//! ```
63
64mod ingest;
65mod runner;
66
67pub use ingest::{
68    IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
69};
70pub use runner::{
71    AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
72    check_reported_path, check_request, worktree_id,
73};
74
75/// Lowercase hex SHA-256 of `bytes`.
76///
77/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
78/// was derived from, and for deriving an opaque worktree id from a path.
79#[must_use]
80pub fn sha256_hex(bytes: &[u8]) -> String {
81    use sha2::{Digest, Sha256};
82    let digest = Sha256::digest(bytes);
83    let mut out = String::with_capacity(64);
84    for byte in digest {
85        use std::fmt::Write as _;
86        let _ = write!(out, "{byte:02x}");
87    }
88    out
89}
90
91#[cfg(test)]
92mod tests {
93    use super::sha256_hex;
94
95    #[test]
96    fn hashes_the_known_vector() {
97        assert_eq!(
98            sha256_hex(b"abc"),
99            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
100        );
101    }
102}