Skip to main content

secureops_core/
util.rs

1//! Small, I/O-free helpers shared across the workspace.
2//!
3//! These were independently re-implemented in the monitors, fs, cli, napi and
4//! checks crates (RFC3339 time formatting, epoch-millis parsing, `path.basename`,
5//! the unix "group/other-accessible" permission test). Centralizing them here
6//! keeps one source of truth so the behaviors cannot drift apart.
7
8use time::{format_description::well_known::Rfc3339, OffsetDateTime};
9
10/// Current UTC time as an RFC3339 string - the Rust equivalent of TS
11/// `new Date().toISOString()` (PRODUCT.md A.5 wire format). On the (impossible)
12/// formatting error, falls back to the epoch so an audit never aborts over a clock.
13pub fn now_iso() -> String {
14    OffsetDateTime::now_utc()
15        .format(&Rfc3339)
16        .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
17}
18
19/// Current UTC time as epoch milliseconds.
20pub fn now_ms() -> i128 {
21    OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000
22}
23
24/// Parse an RFC3339 timestamp to epoch milliseconds. Returns `None` for
25/// unparseable input (callers exclude such entries, matching the TS tool).
26pub fn parse_ms(ts: &str) -> Option<i128> {
27    OffsetDateTime::parse(ts, &Rfc3339)
28        .ok()
29        .map(|t| t.unix_timestamp_nanos() / 1_000_000)
30}
31
32/// Final path component - port of Node's `path.basename`. Trailing slashes are
33/// trimmed first (`"a/b/" -> "b"`); a path with no separator returns itself.
34pub fn basename(p: &str) -> &str {
35    p.trim_end_matches('/').rsplit('/').next().unwrap_or(p)
36}
37
38/// True if a unix permission `mode` grants any group or other access
39/// (`mode & 0o077 != 0`) - the credential-permission red flag used by the
40/// checks, hardening and credential monitor. `mode` is the permission bits,
41/// already masked to `0o777` by the caller.
42pub fn is_group_or_other_accessible(mode: u32) -> bool {
43    mode & 0o077 != 0
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn basename_handles_trailing_slash_and_bare_names() {
52        assert_eq!(basename("/a/b/c.json"), "c.json");
53        assert_eq!(basename("/a/b/"), "b");
54        assert_eq!(basename("bare"), "bare");
55        assert_eq!(basename(""), "");
56    }
57
58    #[test]
59    fn group_or_other_access_matches_octal_mask() {
60        assert!(!is_group_or_other_accessible(0o600));
61        assert!(is_group_or_other_accessible(0o640)); // group read
62        assert!(is_group_or_other_accessible(0o604)); // other read
63        assert!(!is_group_or_other_accessible(0o700));
64    }
65
66    #[test]
67    fn parse_ms_round_trips_and_rejects_garbage() {
68        assert_eq!(parse_ms("1970-01-01T00:00:01Z"), Some(1000));
69        assert_eq!(parse_ms("not-a-timestamp"), None);
70    }
71}